Flutter hooks are powerful functions that streamline state management, side effects handling, and code organization in Flutter applications. Inspired by React hooks, they provide a more concise and modular approach compared to traditional StatefulWidget and setState patterns.
By the end of this guide, you’ll understand the core hooks in Flutter, how to use them effectively, how to create your own custom hooks, and best practices for using them in real-world projects.
Table of Contents
Prerequisites
Before diving into Flutter hooks, make sure you have the following:
Flutter SDK: Installed and configured (Flutter 3.x or higher recommended). Verify with:
flutter --versionDart SDK: Comes with Flutter, ensure it’s up to date.
IDE: Visual Studio Code, Android Studio, or IntelliJ with Flutter extensions.
Basic Flutter knowledge: Familiarity with widgets,
StatelessWidget,StatefulWidget, and state management basics.Package dependency: The
flutter_hookspackage installed by adding the following topubspec.yaml:dependencies: flutter_hooks: ^0.21.3+1Then run:
flutter pub get
Why Flutter Hooks?
Here are some of the benefits of using Flutter hooks:
Improved Readability and Maintainability
Hooks reduce boilerplate by embedding state and side effects logic directly in the widget’s build method. This makes the code cleaner and easier to understand.Reusability
Hooks can be abstracted into custom hooks. For example, you could extract complex logic (like data fetching) into a reusable function.Granular State Management
Instead of managing a singleStateobject for an entire widget, hooks let you manage small, independent pieces of state. This is especially useful for complex UIs.Simplified Side Effects
Hooks such asuseEffectprovide an elegant way to handle lifecycle-related tasks like data fetching, listeners, or subscriptions.
Common Flutter Hooks
Let’s go through the most common hooks, with explanations line by line.
How to Use the useState Hook in Flutter
The simplest and most used hook. It allows you to declare and manage state inside a HookWidget.
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class CounterButton extends HookWidget {
@override
Widget build(BuildContext context) {
final counter = useState<int>(0); // Step 1: create state with initial value 0
return ElevatedButton(
onPressed: () => counter.value++, // Step 2: update state using counter.value
child: Text('Count: ${counter.value}'), // Step 3: read the state
);
}
}
Explanation
useState<int>(0)initializes state with a value of0.counter.valuereads the state.Updating
counter.valuetriggers a rebuild, just likesetState.
How to Use the useAnimationController Hook in Flutter
Handles animations while managing the controller’s lifecycle automatically.
class AnimatedBox extends HookWidget {
@override
Widget build(BuildContext context) {
final controller = useAnimationController(
duration: const Duration(seconds: 1), // Step 1: define animation duration
);
return FadeTransition(
opacity: controller, // Step 2: bind controller to animation
child: Container(width: 100, height: 100, color: Colors.blue),
);
}
}
Explanation
The hook creates an
AnimationControllerthat lasts 1 second.The controller is automatically disposed of when the widget is removed.
You can trigger animations with
controller.forward()orcontroller.reverse().
How to Use the useEffect Hook in Flutter
Handles side effects such as fetching data or setting up listeners.
class DataWidget extends HookWidget {
@override
Widget build(BuildContext context) {
useEffect(() {
fetchData(); // Step 1: perform side effect
return () => cancelSubscription(); // Step 2: optional cleanup
}, []); // Step 3: dependency list
return Text('Data is loading...');
}
}
Explanation
The callback runs when the widget builds.
The cleanup function runs when the widget is disposed or dependencies change.
The empty dependency list
[]means it only runs once.
How to Use the useMemoized Hook in Flutter
Caches expensive computations and reuses results unless dependencies change.
final calculatedValue = useMemoized(() => calculateExpensiveValue(), []);
Explanation
calculateExpensiveValue()runs once and caches the result.With dependencies provided, the function reruns only when they change.
How to Use the useRef Hook in Flutter
Keeps a mutable reference across rebuilds.
final textController = useRef(TextEditingController());
TextFormField(
controller: textController.value,
decoration: InputDecoration(labelText: 'Username'),
);
Explanation
useRefstores an object without triggering rebuilds.Useful for controllers, focus nodes, or mutable values that shouldn’t reset.
How to Use the useCallback Hook in Flutter
Memoizes a callback to prevent unnecessary widget rebuilds.
final onPressed = useCallback(() => print('Pressed'), []);
Explanation
Without
useCallback, functions may be recreated on each rebuild.Memoized callbacks improve performance when passed to widgets like
ListView.
How to Use the useContext Hook in Flutter
Provides direct access to BuildContext values like themes or providers.
final theme = useContext();
How to Use the useTextEditingController Hook in Flutter
A shorthand for creating text controllers.
final usernameController = useTextEditingController();
TextFormField(
controller: usernameController,
decoration: InputDecoration(labelText: 'Username'),
);
Explanation:
1. What is useTextEditingController()?
final usernameController = useTextEditingController();
Normally in Flutter, if you want to manage text input, you create a
TextEditingController.With a normal
StatefulWidget, you’d do something like:
late TextEditingController usernameController;
@override
void initState() {
super.initState();
usernameController = TextEditingController();
}
@override
void dispose() {
usernameController.dispose();
super.dispose();
}
But with Flutter Hooks, you can replace all that boilerplate with:
final usernameController = useTextEditingController();This hook automatically:
Creates the controller.
Keeps it alive for as long as the widget exists.
Disposes of it when the widget is destroyed.
So you don’t need to manage lifecycle manually anymore.
2. Using the controller in a TextFormField
TextFormField(
controller: usernameController,
decoration: InputDecoration(labelText: 'Username'),
);
This
TextFormFieldis linked to theusernameController.Whatever the user types in the input field will be stored in
usernameController.text.You can read or modify it at any time:
print(usernameController.text); // get typed text usernameController.text = "Anthony"; // set default value
3. How it works together
useTextEditingController()provides a ready-to-useTextEditingControllerwithout the hassle of init/dispose.The
TextFormFielduses this controller to manage user input.This is the hooks way of handling text fields.
Summary
useTextEditingController()→ Hook to create & dispose aTextEditingControllerautomatically.TextFormField(controller: ...)→ Uses that controller to manage and access the text typed in the field.Cleaner and safer than manually handling init/dispose in a
StatefulWidget.
How to Create a Custom Hook in Flutter
You can encapsulate logic in reusable hooks.
Future<String> useFetchData() {
final data = useState<String>('Loading...');
useEffect(() {
Future.microtask(() async {
data.value = await fetchDataFromApi();
});
return null;
}, []);
return data.value;
}
Explanation:
1. Function signature
Future<String> useFetchData()
At first glance, it looks like this function should return a Future<String>.
But in reality, the function does not return a Future, it returns data.value, which is a String.
So the correct signature should really be:
String useFetchData()
Because what you’re returning is the current state of the data, not a Future.
2. State setup
final data = useState<String>('Loading...');
This creates a state variable
datawith an initial value of"Loading...".data.valueholds the actual string value.Updating
data.valuewill cause the widget to rebuild.
3. Effect hook
useEffect(() {
Future.microtask(() async {
data.value = await fetchDataFromApi();
});
return null;
}, []);
useEffectruns once (because the dependency list[]is empty).Inside it, a
Future.microtaskschedules an async task to fetch data.Once the API call finishes,
data.valueis updated with the response fromfetchDataFromApi().Updating
data.valuetriggers a rebuild, so the UI will now show the new data instead of"Loading...".
4. Return value
return data.value;
This returns the current state value (
'Loading...'initially, later replaced by the fetched data).On first build, you’ll get
"Loading...".After the API call finishes, a rebuild happens and now
useFetchData()will return the fetched string.
5. How it works in practice
Imagine this widget code:
class MyWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final result = useFetchData();
return Text(result);
}
}
Step 1 → UI shows "Loading...".
Step 2 → API is called in the background.
Step 3 → When the API response arrives, data.value updates.
Step 4 → Widget rebuilds and now Text(result) displays the fetched data.
Summary
useStateholds the data (Loading...→ fetched result).useEffectruns once to trigger the async fetch.When the fetch finishes, it updates state → widget rebuilds → UI shows new value.
The function should return a
String, notFuture<String>.
Advanced Hooks
useListenable: Works withValueNotifierorChangeNotifier.useDebounced: Debounces input, useful for search fields.usePreviousState(from community libraries): Keeps track of the previous value.
Demonstration: Counter Example with Hooks
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class Counter extends HookWidget {
@override
Widget build(BuildContext context) {
final count = useState<int>(0); // state variable initialized to 0
useEffect(() {
print('Count updated: ${count.value}'); // log whenever count changes
return null; // no cleanup needed
}, [count.value]); // dependency: re-run when count changes
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('You clicked ${count.value} times', style: TextStyle(fontSize: 24)),
ElevatedButton(
onPressed: () => count.value++, // increment state
child: Text('Increment'),
),
],
);
}
}
Explanation:
This code is using the flutter_hooks package to manage state and lifecycle in a functional style instead of the usual StatefulWidget + setState. Let’s break it down step by step:
1. Class definition
class Counter extends HookWidget {
@override
Widget build(BuildContext context) {
...
}
}
CounterextendsHookWidgetinstead ofStatelessWidgetorStatefulWidget.HookWidgetallows you to use hooks (likeuseState,useEffect) directly inside thebuildmethod to manage state and side effects.
2. State with useState
final count = useState<int>(0);
useStateis a hook that creates a piece of state.Here, it initializes
countwith0.countis not just anint, but aValueNotifier<int>(meaning you can readcount.valueand update it by assigning tocount.value).
So initially:count.value = 0.
3. Effect with useEffect
useEffect(() {
print('Count updated: ${count.value}');
return null;
}, [count.value]);
useEffectis used to perform side effects whenever dependencies change.In this case, it runs whenever
count.valuechanges.It prints the updated value to the console each time the counter changes.
The second argument
[count.value]is the dependency list (like React Hooks). Ifcount.valuechanges, this effect runs again.
4. UI
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('You clicked ${count.value} times', style: TextStyle(fontSize: 24)),
ElevatedButton(
onPressed: () => count.value++, // increment state
child: Text('Increment'),
),
],
);
A
Columndisplays two widgets:A
Textwidget showing the number of times the button has been clicked.An
ElevatedButtonthat increments the counter when pressed (count.value++).
Because count is a ValueNotifier, updating count.value automatically triggers a rebuild of the widget.
5. How it works in practice
The app shows: "You clicked 0 times" and a button "Increment".
When you tap the button:
count.valueincreases by 1.The widget rebuilds, showing the updated count.
useEffectruns, printingCount updated: Xto the console.
Summary:
This code is a counter app built using Flutter Hooks.
useStatemanages the counter state.useEffectlistens for changes in the counter and runs a side effect (printing to console).The UI displays the count and a button to increment it.
Best Practices
Use dependency lists correctly with
useEffectanduseMemoized.Don’t over-engineer: Sometimes
StatefulWidgetis simpler.Test thoroughly, especially when side effects are involved.
Extract reusable logic into custom hooks to keep widgets focused.
Hooks vs Stateful Widgets
What are Stateful Widgets?
A StatefulWidget is a widget that can change over time because it holds mutable state.
It’s made up of two classes:
StatefulWidget→ the immutable configuration.State<T>→ the mutable state and logic.
How they work
When something in the state changes, you call
setState().This tells Flutter to rebuild the widget tree with the updated state.
Example:
class Counter extends StatefulWidget {
@override
_CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('You clicked $count times'),
ElevatedButton(
onPressed: () => setState(() => count++),
child: Text('Increment'),
),
],
);
}
}
Key points
Good for simple UI state (like counters, toggles, form fields).
Flutter manages the widget’s lifecycle (init, rebuild, dispose).
You handle initialization in
initStateand cleanup indispose.
In short:
Stateful widgets are the classic way to manage state in Flutter. They’re straightforward for beginners and great for simple use cases. For more complex or reusable state logic, hooks (or state management libraries like BLoC, Riverpod) can be cleaner and more scalable.
Summary:
Hooks: Cleaner, modular, reusable, great for advanced state handling.
Stateful Widgets: Easier for beginners and fine for simple state.