Skip to main content

useState

Represents a single, mutable value of any type. It starts at the provided initial value and can be changed later, triggering a rebuild in the process.

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


Widget build(BuildContext context) {
final count = useState(0); // <- The initial value, used only on first build

return Scaffold(
body: Center(child: Text('${count.value}')), // <- Read the current value
floatingActionButton: FloatingActionButton(
onPressed: () => count.value++, // <- Set the value, triggering a rebuild
child: const Icon(Icons.add),
),
);
}
}

Signature

StateHookState<T> useState<T>(T initialValue, {bool listen = true, HookKeys keys = hookKeysEmpty});
StateHookState<T> useStateLazy<T>(T Function() initialValueProvider, {bool listen = true, HookKeys keys = hookKeysEmpty});

StateHookState<T> implements ListenableMutableValue<T>: read the current value with .value, write it with .value =, or use the .modify() extension for collections. It also exposes mounted and setIfMounted(value) for async callbacks (see Caveats).

  • listen (true by default) - when false, changing the value updates it but does not trigger a rebuild.
  • keys - when the keys change, the state is reset back to initialValue.

useStateLazy is identical, except the initial value is produced by a function on first build instead of being passed directly. Use it when constructing the initial value is expensive.

Use cases

Any value that can change over time and needs to be reflected in the UI or in other hooks, e.g.:

  • Value of a form field
  • Current page of a PageView
  • An instance of a complex object that initializes asynchronously (e.g. a database connection)

Reach for useStateLazy when the initial value is costly to build, so the cost is paid once rather than on every build:

class DraftEditor extends HookWidget {
final String template;

const DraftEditor({super.key, required this.template});


Widget build(BuildContext context) {
// buildInitialDraft runs once, not on every build.
final draft = useStateLazy(() => buildInitialDraft(template));

return Text(draft.value);
}
}

String buildInitialDraft(String template) => template.toUpperCase();

If the value is derived from other state rather than owned, prefer useMemoized over a useState that you keep writing to from an effect.

Caveats

  • The initial value is only used on the first build. After that it is ignored, even if it changes.

    final stateA = useState(0);

    // DON'T
    final stateB = useState(stateA.value);

    // ...
    onPressed: () => stateA.value = 1; // <- This won't affect the value of stateB

    To reset the state when an input changes, pass keys instead - the state returns to initialValue whenever the keys change:

    class FilteredList extends HookWidget {
    final String categoryId;

    const FilteredList({super.key, required this.categoryId});


    Widget build(BuildContext context) {
    // Resets back to null whenever categoryId changes.
    final selectedId = useState<String?>(null, keys: [categoryId]);

    return Text(selectedId.value ?? 'Nothing selected');
    }
    }
  • The value can't be changed during the build. This is similar to how setState can't be called in the build method of a Widget.

    final state = useState(0);

    // DON'T
    state.value++; // <- Will throw

    // DO
    useEffect(() {
    state.value++; // <- Won't throw since effects are called after the build
    return null;
    });

    // DO
    onPressed: () => state.value++; // <- Won't throw since this is called as a result of user interaction
  • If the stored object is mutated directly, the hook won't be rebuilt. This is why it's recommended to use immutable objects with useState. With standard Dart mutable collections (List, Map, etc.) you have to make a copy before mutating.

    final state = useState([0]);

    // DON'T
    onPressed: () => state.value.add(1); // <- This won't trigger a rebuild

    // DO
    onPressed: () => state.value = [...state.value, 1]; // <- This will trigger a rebuild

    Copying the collection on every mutation is suboptimal and error-prone, so prefer immutable collections, like those from package:fast_immutable_collections.

    // DO
    final state = useState(IList([0]));

    onPressed: () => state.value = state.value.add(1); // <- This will also trigger a rebuild
  • If a new value is equal (via ==) to the previous value, the widget won't be rebuilt. This avoids unnecessary rebuilds, but it can cause problems with objects that implement == incorrectly.

  • In some cases there is no need to rebuild when the value changes, e.g. when the value is only read inside a callback. Pass listen: false to disable the rebuild:

    // CAREFUL
    final state = useState(0, listen: false);

    onPressed: () => print("The button has been pressed ${state.value++} times");

    Use this carefully: it can lead to unexpected behavior if the code expects a rebuild after the value changes.

  • Setting .value after the hook has been unmounted throws in debug mode. In async callbacks that may complete after dispose, guard with setIfMounted, which sets the value only if still mounted and returns whether it was:

    final progress = useState(0.0);

    Future<void> track(Stream<double> events) async {
    await for (final p in events) {
    if (!progress.setIfMounted(p)) return; // <- Stop once unmounted
    }
    }
caution

useRef is deprecated - it is exactly useState(listen: false). Use useState(listen: false) directly.

See also

  • useMemoized - for values derived from other state, instead of a manually-updated useState
  • useEffect - for running side effects when a state value changes
  • useIsMounted - the guard behind setIfMounted
  • Common hooks - useState in the context of the wider hook set