Basics
Hooks are functions that represent a single piece of state (or business logic). They return a value that can be
then used in UI or other hooks and can request to be rebuilt (like setState
in StatefulWidget
s).
The three most basic hooks are:
useState
which represents a single, mutable value of any type (e.g. the current value of a switch)useEffect
which represents a side effect that can happen in reaction to changing state (e.g. fetching data from the internet when the search field content changes). Effect can optionally return a "dispose" function which will be called when the effect is removed (e.g. to cancel the network request).
The simplest way to start using hooks is via HookWidget
which is like a StatelessWidget
, but with the possibility to
call hooks in its build
method:
class CounterButton extends HookWidget {
Widget build(BuildContext context) {
// Create a mutable state of type `int` with an initial value of 0
final counter = useState(0);
// Register a side-effect
useEffect(() {
print('Counter changed to ${counter.value}'); // <- This will print whenever value of `counter` changes
}, [counter.value]); // <- The "keys" of the effect; it executes when any of them changes.
// Register a one-time side-effect
useEffect(() {
print('Counter created'); // <- This will print once when the widget is created.
// Return the "dispose" function which will be called when this effect is removed.
return () => print('Counter destroyed'); // <- This will print once when the widget is destroyed.
}); // <- No keys is equivalent to empty dependencies - the effect will only run once.
return ElevatedButton(
child: Text('Counter: ${counter.value}'), // Access the current value of the state.
onPressed: () => counter.value++, // Update the value of the state in reaction to user interaction.
);
}
}
Hook rules
Using hooks is simple, but there are a few rules that need to be followed:
- Hooks MUST be called directly in supported places (like the
build
method of aHookWidget
), or in other hooks. Directly - meaning that they cannot be called in callbacks (likeonPressed
of aElevatedButton
). - Hooks CAN'T be called in
if
statements or in loops. Essentially, the same set of hooks must be called in the same order on every build. - Hooks SHOULD start with
use
prefix. This is a convention that makes it easier to distinguish hooks from other functions. - Hooks SHOULD operate on and return immutable objects. This makes it easier to reason about the code and prevents accidental bugs.
Composing hooks
Hooks are composable, meaning that more complex hooks can be built from simpler ones. This is similar to how Widget
s
are composed to create arbitrarily complex UIs. Using this principle, a single useCounterState
hook can be extracted
from the previous example:
// Immutable object representing the state of a counter.
class CounterState {
final int value;
final void Function() onPressed; // An action to be called when the button is pressed.
const CounterState({required this.value, required this.onPressed});
}
// A hook that returns a `CounterState` object.
CounterState useCounterState() {
final counter = useState(0);
useEffect(() {
print('Counter changed to ${counter.value}');
}, [counter.value]);
return CounterState(
value: counter.value,
onPressed: () => counter.value++,
);
}
class CounterButton extends HookWidget {
Widget build(BuildContext context) {
final state = useCounterState();
return ElevatedButton(
child: Text('Counter: ${state.value}'),
onPressed: state.onPressed,
);
}
}
See also
- Common hooks - A list of most common hooks with an in-depth explanation.
- Local state - A guide to managing local state of a screen or other component using hooks.
- Counter example - Implementation of Flutter's "Counter" example using hooks.