Zero Glue Code. Streamless Reactivity. Pure Hook Composition.
If you’ve built Flutter applications using flutter_hooks, you know how empowering hook-based composition can be. By replacing boilerplate StatefulWidget lifecycles with declarative hooks like useState, useMemoized, and useEffect, Flutter Hooks makes widget code remarkably concise.
However, when developers tried bridging classic flutter_bloc into a HookWidget, friction quickly emerged. Because classic BLoC states are published asynchronously across Stream pipelines, consuming state or reacting to side-effects required either:
- Nesting heavy widget wrappers (
BlocBuilder,BlocListener,BlocSelector) inside thebuild()method of aHookWidget. - Depending on third-party, dedicated glue-code packages (such as
flutter_hooks_bloc) that wrapped BLoC streams into custom hooks likeuseBloc,useBlocBuilder, anduseBlocListener.
With BlocSignal, that paradigm changes completely.
💡 The Core Architectural Insight
Unlike classic BLoC, where state updates stream asynchronously through microtask event queues, BlocSignal builds on Rody Davis’s signals.dart primitives.
In BlocSignal:
-
bloc.stateis natively aReadonlySignal<StateType>. - State transitions propagate synchronously in the exact frame they are emitted.
- Equal states are automatically de-duplicated using
==equality.
Because bloc.state is already a native ReadonlySignal, BlocSignal does not need a custom flutter_hooks_bloc_signals adapter package!
Instead, any HookWidget can consume, derive, or react to BlocSignal state out-of-the-box using the official signals_hooks package.
🪝 Key Capabilities & Patterns
Let me walk you through how BlocSignal and signals_hooks simplify state management in HookWidget implementations.
1. Direct State Consumption (useSignalValue / useWatch)
In classic BLoC with Flutter Hooks, subscribing to a BLoC required a useBlocBuilder(bloc) hook or a nested BlocBuilder widget.
With BlocSignal + signals_hooks, bloc.state is simply a signal. You can read and subscribe to it using useSignalValue(bloc.state) or useWatch(bloc.state):
final count = useSignalValue(bloc.state);
Enter fullscreen mode Exit fullscreen mode
Whenever bloc.emit() updates the state, useSignalValue registers a dependency on the hook element and triggers a synchronous rebuild.
2. Streamless Side Effects (useSignalEffect)
In classic BLoC, executing one-off side effects (such as showing a SnackBar, triggering navigation, or logging analytics) forced you to wrap your UI in a BlocListener or call useBlocListener.
Because BlocSignal updates synchronously, you can write inline reactive side effects using useSignalEffect:
useSignalEffect(() {
// Reading `bloc.stateValue` (or `bloc.state.value`) inside the callback
// automatically registers `bloc.state` as a reactive dependency!
if (bloc.stateValue > 10) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Threshold reached!')),
);
}
});
Enter fullscreen mode Exit fullscreen mode
No widget tree nesting required. The effect callback executes synchronously upon transition and automatically unsubscribes when the HookWidget unmounts.
💡 Bonus Tip:
BlocSignalBaseprovides the convenient.stateValuegetter (which delegates directly tobloc.state.value). You can usebloc.stateValueinsideuseSignalEffect,useComputed, or event handlers likeemit(stateValue + 1)for clean, concise state access!
3. Fine-Grained Derived Selectors (useComputed)
Filtering rebuilds in classic BLoC required setting up BlocSelector<MyBloc, MyState, SelectedType> or calling useBlocSelector.
With BlocSignal + signals_hooks, fine-grained selector logic is written cleanly using useComputed:
final isEven = useComputed(() => bloc.stateValue.isEven);
Enter fullscreen mode Exit fullscreen mode
useComputed creates an inline derived signal. Because signals de-duplicate identical outputs, your widget will only rebuild when count.isEven toggles between true and false — avoiding unnecessary builds when count increments from 2 to 4.
4. Lifecycle Teardown & Scope Safety
In typical production applications, BLoCs are provided upstream in the widget tree using BlocSignalProvider. When consuming a provided BLoC inside a HookWidget, zero creation or teardown boilerplate is needed:
// Provided upstream via BlocSignalProvider — Provider handles lifetime & close() automatically!
final bloc = context.read<CounterBloc>();
Enter fullscreen mode Exit fullscreen mode
Widget-Local BLoC Instances
If a HookWidget happens to own its own local BLoC, you can instantiate and dispose of it using standard Flutter Hooks primitives:
// Create once for the widget's lifetime & close when unmounted
final bloc = useMemoized(() => CounterBloc());
useEffect(() => bloc.close, [bloc]);
Enter fullscreen mode Exit fullscreen mode
💡 Quick Tip: If your app uses many widget-local BLoCs, you can define a simple 3-line helper once in your codebase:
B useCreateBloc<B extends BlocSignalBase>(B Function() builder) { final bloc = useMemoized(builder); useEffect(() => bloc.close, [bloc]); return bloc; }This gives you single-line local instantiation (
final bloc = useCreateBloc(() => CounterBloc());) without needing an extra third-party package!
💻 Side-by-Side Comparison
Let's compare classic flutter_hooks_bloc with modern BlocSignal + signals_hooks.
❌ Before: Legacy flutter_hooks_bloc Glue Code
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_hooks_bloc/flutter_hooks_bloc.dart';
class LegacyCounterView extends HookWidget {
const LegacyCounterView({super.key});
@override
Widget build(BuildContext context) {
// Requires specialized third-party useBloc hook
final bloc = useBloc<CounterBloc, int>();
// Requires specialized hook for state rebuilds
final count = useBlocBuilder(bloc);
// Requires specialized hook for side effects
useBlocListener<CounterBloc, int>(bloc, (context, state) {
if (state > 10) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Count is high!')),
);
}
});
return Scaffold(
body: Center(child: Text('Count: $count')),
floatingActionButton: FloatingActionButton(
onPressed: () => bloc.add(Increment()),
child: const Icon(Icons.add),
),
);
}
}
Enter fullscreen mode Exit fullscreen mode
✅ After: Modern BlocSignal + signals_hooks
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:signals_hooks/signals_hooks.dart';
import 'package:bloc_signals/bloc_signals.dart';
class ModernCounterView extends HookWidget {
const ModernCounterView({super.key});
@override
Widget build(BuildContext context) {
// 1. Memoize the BLoC for the widget's entire lifetime & teardown on unmount
final bloc = useMemoized(() => CounterBloc());
useEffect(() => bloc.close, [bloc]);
// 2. Read & watch BLoC state directly as a signal
final count = useSignalValue(bloc.state);
// 2. Inline reactive side effects (no listener widget wrapper needed!)
useSignalEffect(() {
if (bloc.stateValue > 10) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Count is high!')),
);
}
});
// 3. Inline fine-grained computed selector (no selector widget needed!)
final isEven = useComputed(() => bloc.stateValue.isEven);
return Scaffold(
body: Center(
child: Text('Count: $count (Even: ${isEven.value})'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => bloc.add(Increment()),
child: const Icon(Icons.add),
),
);
}
}
Enter fullscreen mode Exit fullscreen mode
📊 Architectural Mapping Table
| Pattern / Goal | Legacy flutter_hooks_bloc
|
Modern BlocSignal + signals_hooks
|
Key Advantage |
|---|---|---|---|
| Accessing BLoC | useBloc<B, S>() |
context.read<MyBloc>() or useMemoized
|
Standard provider read or hook; zero custom BLoC lifecycle package |
| State Rebuilds | useBlocBuilder(bloc) |
useSignalValue(bloc.state) |
Native signal subscription; works with any ReadonlySignal<S>
|
| Side Effects | useBlocListener(bloc, fn) |
useSignalEffect(() => ...) |
Inlined reactive effect; zero widget tree wrappers |
| State Selection | useBlocSelector(bloc, fn) |
useComputed(() => ...) |
Functional signal derivation with automatic == equality filtering |
| Glue Packages | flutter_hooks_bloc |
None (signals_hooks only) |
Zero custom adapter packages required |
🎯 Conclusion
By grounding the BLoC pattern on synchronous, reactive signals, BlocSignal harmonizes two of Flutter’s most powerful state management paradigms: the predictable event-driven architecture of BLoC and the clean, functional composition of Flutter Hooks.
You no longer need dedicated glue-code adapter packages or deep widget tree nesting to use BLoC inside HookWidgets. With signals_hooks, your BLoC states, computed selectors, and side effects fit naturally into standard hook workflows.
🔗 Explore More
- 📦
bloc_signalson pub.dev: pub.dev/packages/bloc_signals - 🪝
signals_hookson pub.dev: pub.dev/packages/signals_hooks - 🐙 GitHub Repository: github.com/RandalSchwartz/BlocSignal
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.