English | 日本語
A Flutter monorepo that brings React's Suspense, Error Boundary, and useTransition primitives to Flutter — declarative async UI without FutureBuilder ladders or manual loading-state plumbing.
In React, you throw a promise from render to suspend a component, you wrap it in <Suspense> to show a fallback, you wrap it in an <ErrorBoundary> to recover from errors, and you call startTransition() from useTransition to keep the previous UI on screen while a new state is being prepared. These four packages give you the same primitives in Flutter — the API surface is intentionally a near-literal mapping:
| React | This monorepo |
|---|---|
<Suspense fallback={...}> |
AsyncZone(fallback: ...) |
use(promise) (React 19) |
AsyncZone.of(context).use(future) (or useAsyncZone().use(future)) |
<ErrorBoundary> |
ErrorBoundary(builder: ..., child: ...) |
componentDidCatch / getDerivedStateFromError |
Same names on ErrorZoneWidget<T> lifecycle |
useTransition / startTransition |
TransitionBoundary + TransitionZone.of(context).startTransition(...) |
useTransition's isPending |
TransitionZone.of(context).isPending |
React Hooks (useState, useEffect, ...) + Suspense |
flutter_hooks + hooks_async_zone (HookZoneWidget, useAsyncZone()) |
Throw a Future from a zone-aware widget's build() — the enclosing AsyncZone shows its fallback, errors propagate to the nearest ErrorBoundary, and a surrounding TransitionBoundary can hold the previous subtree on screen instead of flashing the fallback. The semantics are React's; only the keyword changes (Future instead of Promise).
The API surface mirrors React's, but Flutter's rendering model is not. Two architectural differences cascade into everything else:
- No render interruption. React's
useTransitioncan discard a partial render mid-tree — that is concurrent rendering. Flutter renders synchronously, soTransitionBoundarysimulates the visible part (previous subtree stays on screen,isPendingflips on) but cannot abandon work that has already started. The same constraint rules out auseDeferredValueequivalent — there is no time-slicing to defer onto. - No
Futurecancellation. Dart'sFuturehas no cancel primitive. When a suspended subtree unmounts or a transition supersedes an in-flight fetch, the bridge just stops tracking the future — the underlying I/O keeps running. UseCancelableOperationfrompackage:asyncif you need real cancellation.
use(future) itself is identity-based, same as React's use(promise) — pass a stable Future instance (late final, useMemoized, parent state) or it suspends forever. For value-based caching by a query key, layer a state-management library on top; the fquery_sample / tanstack_query_sample / riverpod_sample examples show small bridges, mirroring React's split between use() and TanStack Query / SWR.
| Package | Description |
|---|---|
async_zone |
Core. AsyncZone, ZoneWidget / StatefulZoneWidget / SliverZoneWidget / ZoneBuilder, and the ErrorZoneWidget lifecycle base. Everything else depends on this. |
async_error_boundary |
High-level ErrorBoundary widget — builder(context, error, reset), onError / onReset callbacks, resetKeys auto-reset, manual showBoundary. |
async_transition_boundary |
React useTransition-style transitions. Wraps a subtree with TransitionBoundary so descendant suspends are absorbed without flashing the surrounding AsyncZone fallback. |
hooks_async_zone |
flutter_hooks integration — HookZoneWidget, useAsyncZone(), sliver/error variants. Use this if your codebase already uses hooks. |
Install the pieces you need. Most apps want at least async_zone + async_error_boundary:
flutter pub add async_zone async_error_boundary
# Optional, pick as needed:
flutter pub add async_transition_boundary
flutter pub add hooks_async_zoneMinimal example — a suspending data card with an error fallback:
import 'package:async_zone/async_zone.dart';
import 'package:async_error_boundary/async_error_boundary.dart';
import 'package:flutter/material.dart';
class UserCard extends ZoneWidget {
const UserCard({super.key, required this.future});
final Future<User> future;
@override
Widget build(BuildContext context) {
// Throws `future` until it resolves — the enclosing AsyncZone shows
// its fallback. Errors propagate to the enclosing ErrorBoundary.
final user = AsyncZone.of(context).use(future);
return Text(user.name);
}
}
// usage
final userFuture = fetchUser(1); // hold the same Future instance across rebuilds
ErrorBoundary(
builder: (context, error, reset) => ErrorView(error: error, onRetry: reset),
child: AsyncZone(
fallback: const CircularProgressIndicator(),
child: UserCard(future: userFuture),
),
)| You want… | Add |
|---|---|
Suspense + use() only |
async_zone |
A drop-in ErrorBoundary widget (recommended over hand-rolling ErrorZoneWidget) |
async_zone + async_error_boundary |
React useTransition-style "keep previous UI while new state suspends" |
+ async_transition_boundary |
Already using flutter_hooks |
+ hooks_async_zone (provides HookZoneWidget, useAsyncZone()) |
Each package's own README has detailed API references, common pitfalls (especially around use()'s identity-based caching), and longer examples.
Everything is built on a single primitive — the ZoneElement mixin in async_zone. Any Element that mixes it in catches Future and Object throws during build() and routes them to the appropriate provider via InheritedWidget lookup:
- A thrown
Futuregoes toAsyncZoneProvider(set up byAsyncZone) → fallback rendering, identity-based result cache keyed on the future instance. - An
Objecterror goes toErrorZoneProvider(set up byErrorZoneWidget/ErrorBoundary) → fallback builder, escalation to outer error zones. - During an active transition, the future is registered with
TransitionZoneProvider(set up byTransitionBoundary) instead of the async fallback — the previous subtree stays visible,isPendingflips on.
Composition is "via inheritance" in two senses: Element mixin composition (e.g. HookZoneWidget = HookElement + ZoneElement, ConsumerZoneWidget in the riverpod sample = ConsumerStatefulElement + ZoneElement), and InheritedWidget nesting (zones / boundaries / transitions can nest, with the same outer-catches-what-inner-cannot semantics as React's boundaries).
The examples/ directory has six runnable samples. The first three demonstrate the core packages directly; the latter three show how state-management libraries can plug into the same Suspense pattern via small bridge implementations:
| Sample | Demonstrates |
|---|---|
async_zone_sample |
Reference app — AsyncZone + use() + ErrorBoundary + TransitionBoundary with hooks_async_zone. |
stateful_zone_sample |
Same UX as above with plain StatefulZoneWidget (no Hooks dependency). |
sliver_zone_sample |
Same UX inside a CustomScrollView using SliverStatefulZoneWidget. |
fquery_sample |
fquery bridge — Suspense + use() over a TanStack-style cache via a useAsyncZoneQuery hook. |
tanstack_query_sample |
tanstack_query bridge — same pattern as fquery_sample, against the Dart port of TanStack Query. |
riverpod_sample |
Riverpod bridge — ConsumerZoneWidget (ConsumerStatefulElement + ZoneElement) + watchOrSuspend. |
The fquery / tanstack / riverpod samples are particularly useful as reference patterns for adding Suspense to any reactive state library: the bridge is usually small (~60 lines).
This repo is a Dart workspace (pubspec.yaml) — all packages and examples resolve from a single lockfile:
flutter pub get # resolves the whole workspace
flutter test # from any package directoryEach example can be launched by cd-ing into it and running flutter run.
- React's Suspense and the
use()hook - React's Error Boundary and
react-error-boundary - React's
useTransition
BSD 3-Clause — see LICENSE.