useFieldState
Holds a single text field's value together with its validation error message. It is the String specialization of useGenericFieldState, returning a MutableFieldState whose value is always a String.
class CommentField extends HookWidget {
const CommentField({super.key});
Widget build(BuildContext context) {
final comment = useFieldState(); // <- T fixed to String, initialValue defaults to ""
return Column(
children: [
TextField(onChanged: (it) => comment.value = it), // <- Write the value
Text('${comment.value.length} characters'), // <- Read it back; rebuilds on change
],
);
}
}
Signature
MutableFieldState useFieldState({String? initialValue}); // initialValue defaults to ""
MutableFieldState is an alias for MutableGenericFieldState<String>. It implements MutableValue<String> and Validatable<String>:
value- read and write the current text; writing triggers a rebuild.errorMessage- aValidatorResult?(aString Function(BuildContext)?), so the message resolves at render time and the state hook stays free ofBuildContext.validate(validator)- runs the validator against the current value, stores the result inerrorMessage, and returns whether the field is valid.
useFieldState() is exactly useGenericFieldState<String>(initialValue: initialValue ?? ""). Reach for it for text fields; use useGenericFieldState<T> when the field holds something other than a String.
Use cases
- Any text input - email, name, search query. The value drives the field, the error message drives the validation display, and
validate()plugs into a submit'sshouldSubmit. - The source of truth behind a
TextField. Pair it with theTextEditingControllerWrapperwidget, which owns theTextEditingControllerand keeps it in sync with the field state in both directions:TextEditingControllerWrapper(text: state.nameField, // <- the MutableFieldState is the source of truthbuilder: (controller) => TextField(controller: controller),)
Caveats
-
Never manage a
TextEditingControllerfromuseMemoized+useListenablefor an editable field. The controller's lifecycle (selection, composing region) does not compose with hook rebuilds, and an effect that writescontroller.textstomps on the user's cursor.useFieldStateplusTextEditingControllerWrapperexists precisely to fix this.// DON'T - an effect writing controller.text loses cursor position and fights user inputfinal controller = useMemoized(TextEditingController.new);useEffect(() {controller.text = externalValue; // <- stomps on what the user is typingreturn null;}, [externalValue]);// DO - field state is the source of truth, the wrapper owns the controllerfinal field = useFieldState(initialValue: externalValue); -
errorMessageis a function, not aString. Build it as a closure resolved against aBuildContext, so the message can be localized at render time.// DON'T - a String is not a ValidatorResultfield.errorMessage = 'Required';// DO - a closure resolved at render timefield.errorMessage = (context) => 'Required'; -
The value and the error are independent: changing
.valuedoes not clearerrorMessage. Re-runvalidate()(or assignnull) when you want the error to disappear as the user edits. The deeper validation and submit-integration patterns live on theuseGenericFieldStatepage.
See also
- useGenericFieldState - the generic form this specializes; full validation and submit-gate coverage
- useSubmitState -
validate()slots into itsshouldSubmitgate - useFocusNode - manage the field's focus with the same auto-dispose discipline
- useState - the plain mutable value behind a field that needs no validation