Skip to main content

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 StatefulWidgets).

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:

  1. Hooks MUST be called directly in supported places (like the build method of a HookWidget), or in other hooks. Directly - meaning that they cannot be called in callbacks (like onPressed of a ElevatedButton).
  2. 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.
  3. Hooks SHOULD start with use prefix. This is a convention that makes it easier to distinguish hooks from other functions.
  4. 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 Widgets 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.