Skip to main content

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 - a ValidatorResult? (a String Function(BuildContext)?), so the message resolves at render time and the state hook stays free of BuildContext.
  • validate(validator) - runs the validator against the current value, stores the result in errorMessage, 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's shouldSubmit.
  • The source of truth behind a TextField. Pair it with the TextEditingControllerWrapper widget, which owns the TextEditingController and keeps it in sync with the field state in both directions:
    TextEditingControllerWrapper(
    text: state.nameField, // <- the MutableFieldState is the source of truth
    builder: (controller) => TextField(controller: controller),
    )

Caveats

  • Never manage a TextEditingController from useMemoized + useListenable for an editable field. The controller's lifecycle (selection, composing region) does not compose with hook rebuilds, and an effect that writes controller.text stomps on the user's cursor. useFieldState plus TextEditingControllerWrapper exists precisely to fix this.

    // DON'T - an effect writing controller.text loses cursor position and fights user input
    final controller = useMemoized(TextEditingController.new);
    useEffect(() {
    controller.text = externalValue; // <- stomps on what the user is typing
    return null;
    }, [externalValue]);

    // DO - field state is the source of truth, the wrapper owns the controller
    final field = useFieldState(initialValue: externalValue);
  • errorMessage is a function, not a String. Build it as a closure resolved against a BuildContext, so the message can be localized at render time.

    // DON'T - a String is not a ValidatorResult
    field.errorMessage = 'Required';

    // DO - a closure resolved at render time
    field.errorMessage = (context) => 'Required';
  • The value and the error are independent: changing .value does not clear errorMessage. Re-run validate() (or assign null) when you want the error to disappear as the user edits. The deeper validation and submit-integration patterns live on the useGenericFieldState page.

See also

  • useGenericFieldState - the generic form this specializes; full validation and submit-gate coverage
  • useSubmitState - validate() slots into its shouldSubmit gate
  • useFocusNode - manage the field's focus with the same auto-dispose discipline
  • useState - the plain mutable value behind a field that needs no validation