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(trueby default) - whenfalse, changing the value updates it but does not trigger a rebuild.keys- when the keys change, the state is reset back toinitialValue.
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'Tfinal stateB = useState(stateA.value);// ...onPressed: () => stateA.value = 1; // <- This won't affect the value of stateBTo reset the state when an input changes, pass
keysinstead - the state returns toinitialValuewhenever 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
setStatecan't be called in thebuildmethod of aWidget.final state = useState(0);// DON'Tstate.value++; // <- Will throw// DOuseEffect(() {state.value++; // <- Won't throw since effects are called after the buildreturn null;});// DOonPressed: () => 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'TonPressed: () => state.value.add(1); // <- This won't trigger a rebuild// DOonPressed: () => state.value = [...state.value, 1]; // <- This will trigger a rebuildCopying the collection on every mutation is suboptimal and error-prone, so prefer immutable collections, like those from
package:fast_immutable_collections.// DOfinal 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: falseto disable the rebuild:// CAREFULfinal 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
.valueafter the hook has been unmounted throws in debug mode. In async callbacks that may complete after dispose, guard withsetIfMounted, 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}}
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 -
useStatein the context of the wider hook set