useEffect
Registers a function to run after the build whenever any of the keys change. The function can optionally return a "dispose" callback, called before the next run and when the hook's context is destroyed.
class TickerView extends HookWidget {
final bool isEnabled;
const TickerView({super.key, required this.isEnabled});
Widget build(BuildContext context) {
final seconds = useState(0);
useEffect(() {
if (!isEnabled) return null; // <- No dispose needed when nothing was created
final timer = Timer.periodic(
const Duration(seconds: 1),
(_) => seconds.value++,
);
return timer.cancel; // <- Called on dispose and before the next run
}, [isEnabled]); // <- Re-run only when isEnabled changes
return Text('${seconds.value}');
}
}
Signature
void useEffect(EffectHookDispose Function() effect, [HookKeys keys = hookKeysEmpty]);
void useImmediateEffect(EffectHookDispose Function() effect, [HookKeys keys = hookKeysEmpty]);
effect is the side effect to run. If it returns a void Function(), that function is the dispose callback; any other return value (including null) is ignored. The dispose callback runs:
- When the hook's context is destroyed (e.g. when the widget is removed from the tree)
- When the keys change, before the next effect runs
keys defaults to an empty list, which never changes - so an effect with no keys runs once on mount. To re-run on change, list the values the effect depends on.
useImmediateEffect has the same signature but runs during the build rather than after it. Use it only when the effect must complete before the build finishes (e.g. initializing an object the build needs).
Use cases
- Initialization and cleanup of resources (e.g. opening and closing a database connection)
- Reacting to changes in values from other hooks
- Asynchronous operations (e.g. showing a dialog after a fixed duration)
Caveats
-
An effect only runs when one of its keys changes, so usually you include every value the effect uses in the keys. Sometimes it is useful to omit a value for finer-grained control over when the effect runs.
-
Effects run after the build. This means the effect doesn't block the build and is free to trigger rebuilds (e.g. by setting a
useStatevalue). In the rare case where this is not acceptable - for example, initializing an object that is needed during the build - useuseImmediateEffectinstead.// useEffect runs AFTER the build:useEffect(() {state.value++; // <- A rebuild is scheduled; safereturn null;});// useImmediateEffect runs DURING the build:useImmediateEffect(() {resource.initialize(); // <- Ready before the build reads itreturn resource.dispose;}); -
asyncfunctions can be passed touseEffect, but they are not "waited for". This has consequences:- If the keys change while the
asyncfunction is still running, a second effect runs in parallel. - A dispose function returned from an
asynceffect has no effect. - The
asyncfunction can outlive the context it started in, so guard against updating an unmounted hook withuseIsMounted:
class DelayedGreeting extends HookWidget {const DelayedGreeting({super.key});Widget build(BuildContext context) {final greeting = useState('');final isMounted = useIsMounted();useEffect(() async {await Future<void>.delayed(const Duration(seconds: 1));if (isMounted()) greeting.value = 'Hello'; // <- Guard against post-dispose updatesreturn null;}); // <- No keys: runs once on mountreturn Text(greeting.value);}} - If the keys change while the
-
An effect is a regular closure, so it captures the values of variables at the moment it is created. This can surprise you:
final state = useState(0);final computed = state.value + 1;useEffect(() {// Always prints "1", even after "state" changes, because it captures// "computed" from when the effect was created.final timer = Timer.periodic(Duration(seconds: 1), (_) => print(computed));return timer.cancel;}); // <- No keys, so the effect isn't recreated when "state" changes.If adding the captured value to the keys is not desired, reach for
useValueWrapper, which exposes a stable holder whose.valuealways reflects the latest value. -
For values derived from other state, do not write into a
useStatefrom an effect. UseuseMemoizedinstead - it is the direct way to compute a value and avoids an extra state variable and a redundant rebuild.
See also
- useMemoized - prefer it over
useEffectfor derived values - useIsMounted - guard async effects against post-dispose updates
- useValueWrapper - read fresh values in an effect without re-running it
- Common hooks -
useEffectalongside the rest of the core hooks