About hooks
This page explains what are hooks and how they are related to Riverpod.
"Hooks" are utilities common from a separate package, independent from Riverpod:
flutter_hooks.
Although flutter_hooks is a completely separate package and does not have anything
to do with Riverpod (at least directly), it is common to pair Riverpod
and flutter_hooks together. After-all, Riverpod and flutter_hooks are
maintained by the same team.
Hooks are completely optional. You do not have to use hooks, especially if you
are starting Flutter. They are powerful tools, but not very "Flutter-like".
As such, it may make sense to start first with plain Flutter/Riverpod, and come back
to hooks once you have a bit more experience.
What are hooks?
Hooks are functions used inside widgets. They are designed as an alternative to StatefulWidgets, to make logic more reusable and composable.
Hooks are a concept coming from React, and flutter_hooks
is merely a port of the React implementation to Flutter.
As such, yes, hooks may feel a bit out of place in Flutter. Ideally,
in the future we would have a solution to the problem that hooks solves,
designed specifically for Flutter.
If Riverpod's providers are for "global" application state, hooks are for
local widget state. Hooks are typically used for dealing with stateful UI objects,
such as TextEditingController,
AnimationController.
They can also serve as a replacement to the "builder" pattern, replacing widgets
such as FutureBuilder/TweenAnimatedBuilder
by an alterative that does not invole "nesting" – drastically improving readability.
In general, hooks are helpful for:
- forms
- animations
- reacting to user events
- ...
As an example, we could use hooks to manually implement a fade-in animation, where a widget starts invisible and slowly appears.
If we were to use StatefulWidget, the code would look like this:
class FadeIn extends StatefulWidget {
const FadeIn({Key? key, required this.child}) : super(key: key);
final Widget child;
State<FadeIn> createState() => _FadeInState();
}
class _FadeInState extends State<FadeIn> with SingleTickerProviderStateMixin {
late final AnimationController animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
);
void initState() {
super.initState();
animationController.forward();
}
void dispose() {
animationController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animationController,
builder: (context, child) {
return Opacity(
opacity: animationController.value,
child: widget.child,
);
},
);
}
}
Using hooks, the equivalent would be:
class FadeIn extends HookWidget {
const FadeIn({Key? key, required this.child}) : super(key: key);
final Widget child;
Widget build(BuildContext context) {
// Create an AnimationController. The controller will automatically be
// disposed when the widget is unmounted.
final animationController = useAnimationController(
duration: const Duration(seconds: 2),
);
// useEffect is the equivalent of initState + didUpdateWidget + dispose.
// The callback passed to useEffect is executed the first time the hook is
// invoked, and then whenever the list passed as second parameter changes.
// Since we pass an empty const list here, that's strictly equivalent to `initState`.
useEffect(() {
// start the animation when the widget is first rendered.
animationController.forward();
// We could optionally return some "dispose" logic here
return null;
}, const []);
// Tell Flutter to rebuild this widget when the animation updates.
// This is equivalent to AnimatedBuilder
useAnimation(animationController);
return Opacity(
opacity: animationController.value,
child: child,
);
}
}
There are a few interesting things to note in this code:
There is no memory leak. This code does not recreate a new
AnimationController
whenever the widget rebuild, and the controller is correctly released when the widget is unmounted.It is possible to use hooks as many time as we want within the same widget. As such, we can create multiple
AnimationController
if we want:
Widget build(BuildContext context) {
final animationController = useAnimationController(
duration: const Duration(seconds: 2),
);
final anotherController = useAnimationController(
duration: const Duration(seconds: 2),
);
...
}This creates two controllers, without any sort of negative consequence.
If we wanted, we could refactor this logic into a separate reusable function:
double useFadeIn() {
final animationController = useAnimationController(
duration: const Duration(seconds: 2),
);
useEffect(() {
animationController.forward();
return null;
}, const []);
useAnimation(animationController);
return animationController.value;
}We could then use this function inside our widgets, as long as that widget is a HookWidget:
class FadeIn extends HookWidget {
const FadeIn({Key? key, required this.child}) : super(key: key);
final Widget child;
Widget build(BuildContext context) {
final fade = useFadeIn();
return Opacity(opacity: fade, child: child);
}
}Note how our
useFadeIn
function is completely independent from ourFadeIn
widget.
If we wanted, we could use thatuseFadeIn
function in a completely different widget, and it would still work!
How to use hooks
Hooks comes with unique constraints:
They can only be used within the
build
method of a widget that extends HookWidget:Good:
class Example extends HookWidget {
Widget build(BuildContext context) {
final controller = useAnimationController();
...
}
}Bad:
// Not a HookWidget
class Example extends StatelessWidget {
Widget build(BuildContext context) {
final controller = useAnimationController();
...
}
}Bad:
class Example extends HookWidget {
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
// Not _actually_ inside the "build" method, but instead inside
// a user interaction lifecycle (here "on pressed").
final controller = useAnimationController();
},
child: Text('click me'),
);
}
}They cannot be used conditionally or in a loop.
Bad:
class Example extends HookWidget {
const Example({required this.condition, super.key});
final bool condition;
Widget build(BuildContext context) {
if (condition) {
// Hooks should not be used inside "if"s/"for"s, ...
final controller = useAnimationController();
}
...
}
}
For more information about hooks, see flutter_hooks.