home / skills / rodydavis / skills / signals-and-flutter-hooks

signals-and-flutter-hooks skill

/skills/signals-and-flutter-hooks

This skill helps you manage Flutter state more efficiently by teaching signals, flutter hooks, and reactive patterns for scalable UI.

npx playbooks add skill rodydavis/skills --skill signals-and-flutter-hooks

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
6.9 KB
---
name: signals-and-flutter-hooks
description: Explore state management in Flutter, from the basics of `setState` to advanced techniques using ValueNotifier, Signals, Flutter Hooks, and the new signals_hooks package for a reactive and efficient approach.
metadata:
  url: https://rodydavis.com/posts/signals-and-flutter-hooks
  last_modified: Tue, 03 Feb 2026 20:04:15 GMT
---

# Signals and Flutter Hooks


When working with data in [Flutter](https://flutter.dev), on of the first things you are exposed to is [setState](https://api.flutter.dev/flutter/widgets/State/setState.html).

## setState

```
import 'package:flutter/material.dart';

void main() {
  runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: Counter()));
}

class Counter extends StatefulWidget {
  const Counter({super.key});

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int count = 0;

  void increment() {
    if (mounted) {
      setState(() {
        count++;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(child: Text('Count: $count')),
      floatingActionButton: FloatingActionButton(
        onPressed: increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}
```

This simply marks the widget as dirty every time you call **setState** but requires you (as the developer) to be mindful and explict about when those updates happen. If you forget to call **setState** when mutating data the widget tree can become stale.

## ValueNotifier

We can impove this by using [ValueNotifier](https://api.flutter.dev/flutter/foundation/ValueNotifier-class.html) instead of storing the value directly. This gives us the ability to read and write a value in a container and use helper widgets like [ValueListenableBuilder](https://api.flutter.dev/flutter/widgets/ValueListenableBuilder-class.html) to update sub parts of the widget tree on value changes.

```
import 'package:flutter/material.dart';

void main() {
  runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: Counter()));
}

class Counter extends StatefulWidget {
  const Counter({super.key});

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  final count = ValueNotifier(0);

  void increment() {
    count.value++;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(child: ValueListenableBuilder(
        valueListenable: count,
        builder: (context, value, child) {
          return Text('Count: $value');
        }
      )),
      floatingActionButton: FloatingActionButton(
        onPressed: increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}
```

## FlutterSignal

Using the [signals](https://pub.dev/packages/signals) package we can upgrade ValueNotifier to a [signal backed implmentation](https://preactjs.com/guide/v10/signals/) which uses a reactive graph based on a push / pull architecture.

```
import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart';

void main() {
  runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: Counter()));
}

class Counter extends StatefulWidget {
  const Counter({super.key});

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  final count = signal(0);

  void increment() {
    count.value++;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(
        child: ValueListenableBuilder(
          valueListenable: count,
          builder: (context, value, child) {
            return Text('Count: $value');
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}
```

> Signals created after **6.0.0** also implement ValueNotifier so you can easily migrate them without changing any other code.

Instead of ValueListenableBuilder we can use the Watch widget or .watch(context) extension.

```
import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart';

void main() {
  runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: Counter()));
}

class Counter extends StatefulWidget {
  const Counter({super.key});

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  final count = signal(0);

  void increment() {
    count.value++;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(
        child: Text('Count: ${count.watch(context)}'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}
```

## flutter\_hooks

Using [Flutter Hooks](https://pub.dev/packages/flutter_hooks) we can reduce boilerplate of StatefulWidget by switching to a HookWidget. With **useState** we can define the state directly in the build method and easily share them across widgets.

```
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

void main() {
  runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: Counter()));
}

class Counter extends HookWidget {
  const Counter({super.key});

  @override
  Widget build(BuildContext context) {
    final count = useState(0);
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(child: Text('Count: ${count.value}')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => count.value++,
        child: const Icon(Icons.add),
      ),
    );
  }
}
```

> **useState** returns a ValueNotifier that automatically rebuilds the widget on changes

## signals\_hooks

Using a new package [signals\_hooks](https://pub.dev/packages/signals_hooks) we can now define signals in HookWidgets and have the benifits of a reactive graph with shareable lifecycles between widgets.

```
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:signals_hooks/signals_hooks.dart';

void main() {
  runApp(const MaterialApp(debugShowCheckedModeBanner: false, home: Counter()));
}

class Counter extends HookWidget {
  const Counter({super.key});

  @override
  Widget build(BuildContext context) {
    final count = useSignal(0);
    final countStr = useComputed(() => count.value.toString());
    useSignalEffect(() {
      print('count: $count');
    });
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(child: Text('Count: $countStr')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => count.value++,
        child: const Icon(Icons.add),
      ),
    );
  }
}
```

Overview

This skill explores state management patterns in Flutter, guiding you from the basic setState approach to advanced, reactive techniques using ValueNotifier, the signals package, Flutter Hooks, and the signals_hooks integration. It demonstrates practical migrations and how to reduce boilerplate while improving reactivity and lifecycle sharing across widgets. The examples focus on counters but illustrate patterns applicable to larger apps.

How this skill works

The content compares incremental upgrades to state handling: setState for manual widget invalidation, ValueNotifier for localized listenable state, and signals for a reactive push/pull dependency graph. It shows how Flutter Hooks removes StatefulWidget boilerplate by defining state in the build function and how signals_hooks combines signals with hooks to create reactive values, computed values, and side-effect hooks with shared lifecycles.

When to use it

  • Use setState for small, local widget state with minimal dependencies.
  • Adopt ValueNotifier when you need listenable state for partial subtree updates.
  • Switch to signals when you want a fine-grained reactive graph and deterministic updates.
  • Use Flutter Hooks to simplify lifecycle and reduce StatefulWidget boilerplate.
  • Combine signals_hooks when you need reactive values, computed derivations, and hook-managed lifecycles.

Best practices

  • Start with the simplest tool that meets needs; migrate to signals only when reactive composition adds value.
  • Prefer ValueListenableBuilder or .watch(context) to limit rebuild scope rather than rebuilding entire widgets.
  • Use useComputed for derived values to avoid recomputation in build methods.
  • Keep effects (useSignalEffect) focused and side-effect free where possible; unsubscribe/cleanup through hook lifecycles.
  • Name signals clearly and avoid mutating shared state without explicit intent.

Example use cases

  • Simple counters or toggles that start with setState and upgrade to ValueNotifier for local listening.
  • Form fields where individual inputs update without rebuilding the whole form using ValueNotifier or signals.
  • Dashboard widgets with interdependent metrics computed via useComputed and updated reactively.
  • Shared, short-lived state inside HookWidgets using useSignal to keep lifecycle scoped to the widget tree.
  • Logging or analytics side effects triggered by state changes using useSignalEffect.

FAQ

Do signals replace ValueNotifier and setState?

Signals provide a more reactive model and can implement ValueNotifier, but they are not strictly required for all cases. Use the simplest approach that fits your app's complexity.

Can I mix Hooks and StatefulWidgets?

Yes. Hooks work inside HookWidgets; you can incrementally adopt them and interoperate with existing StatefulWidgets and ValueNotifiers.