Skip to main content

useGenericFieldState

Holds a single form field's value together with its validation error message. It returns a MutableGenericFieldState<T> - a mutable value you read and write through .value, plus an errorMessage and a validate() method.

class CommentBox extends HookWidget {
const CommentBox({super.key});


Widget build(BuildContext context) {
// useFieldState is the String specialization - initialValue defaults to "".
final comment = useFieldState();

return Column(
children: [
TextField(onChanged: (it) => comment.value = it), // <- write the value
Text('${comment.value.length} characters'), // <- read it back, rebuilds on change
],
);
}
}

Signature

MutableGenericFieldState<T> useGenericFieldState<T>({required T initialValue});

// String specialization, documented separately:
MutableFieldState useFieldState({String? initialValue}); // initialValue defaults to ""

MutableGenericFieldState<T> implements MutableValue<T> and Validatable<T>:

  • value - read and write the current field value; writing triggers a rebuild.
  • errorMessage - a ValidatorResult? (a String Function(BuildContext)?), so the message is resolved at render time and the state hook stays free of BuildContext. Read it to show the error, or assign it to set one.
  • validate(validator) - runs the validator against the current value, stores the result in errorMessage, and returns whether the field is valid (true when there is no error).

useFieldState is the String specialization - the same state with T fixed to String and initialValue defaulting to "". It is documented on its own page in the Flutter category; reach for it for text fields and use useGenericFieldState<T> when the field holds something else.

Use cases

  • Any form field. The value drives the input, the error message drives the validation display, and validate() plugs into a submit's shouldSubmit.

  • Non-string fields - a dropdown, a date picker, a toggle - where the field still wants a value plus a validation slot:

    enum Priority { low, medium, high }

    class PrioritySelector extends HookWidget {
    const PrioritySelector({super.key});


    Widget build(BuildContext context) {
    // useGenericFieldState carries any type, not just String.
    final priority = useGenericFieldState<Priority>(initialValue: Priority.medium);

    return DropdownButton<Priority>(
    value: priority.value,
    onChanged: (it) => priority.value = it ?? Priority.medium,
    items: [
    for (final it in Priority.values) DropdownMenuItem(value: it, child: Text(it.name)),
    ],
    );
    }
    }
  • Validation as part of a submit. Run every field in shouldSubmit and abort if any fails - the fields already carry their messages, so no separate isFormValid() helper is needed:

    bool validate() => [
    emailState.validate((it) => isValidEmail(it) ? null : (context) => 'Invalid email'),
    passwordState.validate((it) => it.length >= 8 ? null : (context) => 'Min 8 characters'),
    ].every((it) => it);

    void login() => submitState.runSimple<void, Never>(
    shouldSubmit: () => validate(), // <- the one gate; fields keep their error messages
    submit: () => auth.logIn(emailState.value, passwordState.value),
    );

This field state pairs with the TextEditingControllerWrapper widget, which bridges a MutableFieldState to a Flutter TextField's controller so the two stay in sync without manual onChanged wiring.

Caveats

  • errorMessage is a function, not a String. It is a ValidatorResult? (String Function(BuildContext)?) so localized messages resolve against a BuildContext at render time. Build it as a closure, not a bare string:

    // DON'T - a String is not a ValidatorResult
    emailState.errorMessage = 'Invalid email';

    // DO - a closure resolved at render time
    emailState.errorMessage = (context) => 'Invalid email';
  • Validate in the submit's shouldSubmit, not in a separate pre-check. validate() already sets errorMessage as a side effect, so a manual if (!valid) return before the submit duplicates the gate.

  • Clear stale server-side errors in beforeSubmit. A field error set from a previous failed API call lingers in errorMessage until you reset it (field.errorMessage = null) or validate() overwrites it.

  • The value and the error are independent. Changing .value does not clear errorMessage - re-run validate() (or clear the message) if you want the error to disappear as the user edits.

See also

  • useFieldState - the String specialization for text fields
  • useSubmitState - validate() slots into its shouldSubmit gate
  • useState - the plain mutable value behind a field that needs no validation