Answer
Overview
Flutter Hooks is a package that brings React-style hooks to Flutter, allowing you to manage state and side effects in functional widgets with less boilerplate than StatefulWidget.
Installation
Add to
text
pubspec.yamlyamldependencies: flutter_hooks: ^0.20.0
What Are Hooks?
Hooks are reusable functions that manage widget state, lifecycle, and side effects inside
text
HookWidgetTraditional StatefulWidget:
dartclass Counter extends StatefulWidget { _CounterState createState() => _CounterState(); } class _CounterState extends State<Counter> { int count = 0; Widget build(BuildContext context) { return Text('$count'); } }
With Flutter Hooks:
dartimport 'package:flutter_hooks/flutter_hooks.dart'; class Counter extends HookWidget { Widget build(BuildContext context) { final count = useState(0); // Hook manages state return Text('${count.value}'); } }
Core Hooks
1. useState — State Management
Manages local state (like
text
setStatedartclass Counter extends HookWidget { Widget build(BuildContext context) { final count = useState(0); // Initial value = 0 return Column( children: [ Text('Count: ${count.value}'), ElevatedButton( onPressed: () => count.value++, // Update state child: Text('Increment'), ), ], ); } }
2. useEffect — Side Effects
Runs side effects (like
text
initStatetext
disposedartclass Timer extends HookWidget { Widget build(BuildContext context) { final count = useState(0); useEffect(() { final timer = Timer.periodic(Duration(seconds: 1), (_) { count.value++; }); return timer.cancel; // Cleanup (like dispose) }, []); // Empty list = run once on mount return Text('Elapsed: ${count.value}s'); } }
Dependencies:
dart// Run effect when `userId` changes useEffect(() { fetchUserData(userId); return null; // No cleanup }, [userId]); // Re-run when userId changes
3. useMemoized — Expensive Calculations
Caches computed values (like
text
useMemodartclass ExpensiveList extends HookWidget { Widget build(BuildContext context) { final items = useMemoized(() { return List.generate(1000, (i) => 'Item $i'); // Only computed once }); return ListView(children: items.map((e) => Text(e)).toList()); } }
4. useTextEditingController
Creates and disposes
text
TextEditingControllerdartclass SearchBar extends HookWidget { Widget build(BuildContext context) { final controller = useTextEditingController(); return TextField( controller: controller, onChanged: (value) => print(value), ); // Auto-disposed when widget unmounts } }
5. useAnimationController
Creates
text
AnimationControllerdartclass AnimatedBox extends HookWidget { Widget build(BuildContext context) { final controller = useAnimationController( duration: Duration(seconds: 1), ); return GestureDetector( onTap: () => controller.forward(), child: FadeTransition( opacity: controller, child: Container(width: 100, height: 100, color: Colors.blue), ), ); } }
6. useFuture — Async Data
Handles async operations.
dartclass UserProfile extends HookWidget { Widget build(BuildContext context) { final userSnapshot = useFuture(fetchUser()); if (userSnapshot.connectionState == ConnectionState.waiting) { return CircularProgressIndicator(); } if (userSnapshot.hasError) { return Text('Error: ${userSnapshot.error}'); } return Text('User: ${userSnapshot.data?.name}'); } }
7. useStream — Stream Listening
Subscribes to streams automatically.
dartclass ChatMessages extends HookWidget { Widget build(BuildContext context) { final messagesSnapshot = useStream(messageStream); final messages = messagesSnapshot.data ?? []; return ListView.builder( itemCount: messages.length, itemBuilder: (_, i) => Text(messages[i]), ); } }
Custom Hooks
Create reusable hooks.
dart// Custom hook for debounced search ValueNotifier<String> useDebounce(String value, Duration delay) { final debounced = useState(value); useEffect(() { final timer = Timer(delay, () { debounced.value = value; }); return timer.cancel; }, [value]); return debounced; } // Usage class SearchScreen extends HookWidget { Widget build(BuildContext context) { final query = useState(''); final debouncedQuery = useDebounce(query.value, Duration(milliseconds: 500)); useEffect(() { if (debouncedQuery.value.isNotEmpty) { searchAPI(debouncedQuery.value); } return null; }, [debouncedQuery.value]); return TextField(onChanged: (val) => query.value = val); } }
Comparison: StatefulWidget vs HookWidget
| Feature | StatefulWidget | HookWidget |
|---|---|---|
| Boilerplate | High (2 classes) | Low (1 class) |
| State management | text | text |
| Lifecycle | text text | text |
| Controllers | Manual creation/disposal | Auto-managed |
| Code reuse | Mixins (limited) | Custom hooks ✅ |
| Performance | Same | Same |
Best Practices
dart// ✅ Use hooks at top level (not inside conditionals) class MyWidget extends HookWidget { Widget build(BuildContext context) { final count = useState(0); // ✅ Top level if (count.value > 5) { // final other = useState(0); // ❌ Conditional hook (error) } return Text('$count'); } } // ✅ Prefer HookWidget for simple state // ❌ Don't use for complex state (use BLoC, Riverpod instead) // ✅ Cleanup in useEffect useEffect(() { final subscription = stream.listen(...); return subscription.cancel; // ✅ Cleanup }, []);
When to Use Flutter Hooks
- ✅ Simple local state (counters, toggles, form inputs)
- ✅ Animations (auto-managed controllers)
- ✅ Side effects (API calls, timers, subscriptions)
- ✅ Reusable logic (custom hooks)
- ❌ Complex state (use BLoC, Riverpod, Provider instead)
- ❌ Large apps (hooks are for widget-level state)
Common Hooks Summary
| Hook | Purpose | Example |
|---|---|---|
text | Local state | Counter, toggle |
text | Side effects | API calls, timers |
text | Cache values | Expensive calculations |
text | Async data | API responses |
text | Stream data | Real-time updates |
text | Text input | Search, forms |
text | Animations | Fades, slides |
Learn more: Flutter Hooks Package