Flutter Tutorial

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

  1. Extend HookWidget instead of StatelessWidget.
  2. Call hooks only inside build.
  3. Call them in the same order every time: never inside if or 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

HookReplaces
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.

Practise in the playground Updated