Flutter Intermediate Lesson 49 of 135 3 min read
Flutter Hooks Tutorial: useState, useEffect and Custom Hooks
Learn flutter_hooks with easy runnable examples: HookWidget, useState, useEffect, useTextEditingController and writing your own reusable hooks.
On this page
Flutter Hooks (flutter_hooks) lets you write widgets with local state without a StatefulWidget and its separate State class. Each piece of state or each controller becomes one line, called a hook. Hooks also make it easy to reuse stateful logic between widgets.
If you have used React, this will feel familiar.
Install
flutter pub add flutter_hooks
The counter
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
void main() => runApp(const MaterialApp(home: CounterPage()));
class CounterPage extends HookWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
final count = useState(0); // a ValueNotifier<int> that survives rebuilds
return Scaffold(
appBar: AppBar(title: const Text('Flutter Hooks')),
body: Center(child: Text('${count.value}', style: const TextStyle(fontSize: 48))),
floatingActionButton: FloatingActionButton(
onPressed: () => count.value++,
child: const Icon(Icons.add),
),
);
}
}
Compare this with the setState counter: one class instead of two, no setState.
The rules of hooks
- Extend
HookWidgetinstead ofStatelessWidget. - Call hooks only inside
build. - Call them in the same order every time: never inside
ifor loops.
Controllers without dispose
Hooks create and dispose controllers for you:
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
void main() => runApp(const MaterialApp(home: SearchPage()));
class SearchPage extends HookWidget {
const SearchPage({super.key});
static const cities = ['Kathmandu', 'Pokhara', 'Delhi', 'Toronto', 'Sydney', 'Lisbon'];
@override
Widget build(BuildContext context) {
final controller = useTextEditingController();
final query = useValueListenable(controller); // rebuild as the user types
final results = cities
.where((c) => c.toLowerCase().contains(query.text.toLowerCase()))
.toList();
return Scaffold(
appBar: AppBar(title: const Text('Search')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: TextField(
controller: controller,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.search),
hintText: 'Type a city',
),
),
),
Expanded(
child: ListView(
children: [for (final c in results) ListTile(title: Text(c))],
),
),
],
),
);
}
}
useEffect: run code when something changes
useEffect runs after the first build, and again whenever a value in its key list changes. Return a function to clean up.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
void main() => runApp(const MaterialApp(home: TimerPage()));
class TimerPage extends HookWidget {
const TimerPage({super.key});
@override
Widget build(BuildContext context) {
final seconds = useState(0);
final running = useState(true);
useEffect(() {
if (!running.value) return null;
final timer = Timer.periodic(const Duration(seconds: 1), (_) => seconds.value++);
return timer.cancel; // clean-up when running changes or the widget goes
}, [running.value]);
return Scaffold(
appBar: AppBar(title: const Text('useEffect')),
body: Center(child: Text('${seconds.value} s', style: const TextStyle(fontSize: 48))),
floatingActionButton: FloatingActionButton(
onPressed: () => running.value = !running.value,
child: Icon(running.value ? Icons.pause : Icons.play_arrow),
),
);
}
}
Common hooks
| Hook | Replaces |
|---|---|
useState(initial) | A field + setState |
useEffect(fn, keys) | initState, didUpdateWidget, dispose |
useMemoized(fn, keys) | Caching an expensive value |
useTextEditingController() | Creating and disposing a TextEditingController |
useAnimationController(duration: ...) | An AnimationController + SingleTickerProviderStateMixin |
useScrollController() | A ScrollController |
useFocusNode() | A FocusNode |
useStream(stream) / useFuture(future) | StreamBuilder / FutureBuilder |
useValueListenable(notifier) | ValueListenableBuilder |
Your own hook
A custom hook is just a function whose name starts with use and that calls other hooks:
ValueNotifier<bool> useToggle([bool initial = false]) => useState(initial);
// In a HookWidget:
final isOn = useToggle();
// isOn.value = !isOn.value;
Hooks and app state
Flutter Hooks is for local (per-widget) state. For shared state, combine it with Riverpod using hooks_riverpod.
Pros and cons
- Pros: much less code; controllers are disposed automatically; logic is easy to reuse.
- Cons: another style to learn; the rules of hooks must be followed; some teams prefer plain
StatefulWidget.
Try it yourself
Rewrite the Explicit animations example with useAnimationController so it no longer needs a StatefulWidget.