use-jolt-setup
Use when building Flutter widgets in a setup-once / hooks-driven composition style — declaring signals, controllers, listenables, effects, timers, and lifecycle callbacks through `use*` hooks inside a `setup` function that runs once, owns and disposes those resources, and returns a builder for react
Install
mkdir -p .claude/skills/use-jolt-setup && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/16252" && unzip -o skill.zip -d .claude/skills/use-jolt-setup && rm skill.zipInstalls to .claude/skills/use-jolt-setup
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Use when building Flutter widgets in a setup-once / hooks-driven composition style — declaring signals, controllers, listenables, effects, timers, and lifecycle callbacks through `use*` hooks inside a `setup` function that runs once, owns and disposes those resources, and returns a builder for reactive rebuilds. Provides SetupWidget, SetupMixin (on existing StatefulWidget), and SetupBuilder.About this skill
Use Jolt Setup
package:jolt_setup/jolt_setup.dart brings a Vue/React-style composition model to Flutter on top of Jolt. A widget has two phases:
- Setup phase — a
setupfunction runs once per widget identity. Inside it,use*hooks declare every long-lived resource: signals, controllers, listenables, effects, timers, lifecycle callbacks. The setup function returns aWidgetFunction(() => Widget) that closes over those resources. - Build phase — the returned builder is invoked on every rebuild. Reactive reads (
signal.value,controller.value, etc.) inside the builder are tracked through aPostFrameEffect, so changes coalesce to one rebuild at frame end.
The package re-exports package:jolt_flutter/jolt_flutter.dart, so Signal, Computed, Effect, JoltBuilder, PostFrameEffect, .listenable / .notifier, etc. come along.
All hooks live in references/hooks.md. This file focuses on the three entry surfaces that own a setup runtime: SetupWidget, SetupMixin, and SetupBuilder.
Choosing an Entry Surface
| Situation | Use |
|---|---|
New widget; want a dedicated immutable widget type and reactive props | SetupWidget<T> |
Existing StatefulWidget whose State<T> you must keep (custom methods, mixins, external API) | SetupMixin<T> |
| Local composition, prototypes, leaf widgets — no need for a named widget class | SetupBuilder |
All three share the same setup model below; they only differ in how they bind a setup callback to the Flutter element tree.
The Setup Runtime (shared by all three)
Every entry surface creates a SetupContext that:
- Holds an
EffectScopeso signals/effects created during setup are automatically disposed when the widget unmounts. - Records the ordered list of hooks registered through
useHook/use*. Hot reload reuses hooks by sequence position and runtime type; a mismatch unmounts the tail and re-mounts new hooks. - Owns a
PostFrameEffect"renderer" that wraps the returned builder. Reactive reads inside the builder track this effect, so dependency changes schedulemarkNeedsBuildat frame end. - Forwards Flutter element lifecycle (
didUpdateWidget,didChangeDependencies,activate,deactivate,unmount,reassemble) to each hook, in registration order on activate, reverse order on deactivate/unmount. - Exposes
resetSetup()— schedules a full setup rerun at frame end (tears down hooks + renderer, runssetupagain). Use only when the initialization boundary itself must rebuild; ordinary state changes belong in signals/effects.
Implication: don't put conditional use* calls behind if / loops that can change between rebuilds, and don't call use* outside setup — the hook index must be stable. (Bare reactive reads through .value happen in the builder, not in setup itself, unless you want them as setup-time dependencies of an effect/computed.)
SetupWidget<T>
A new widget base class. Override setup(BuildContext context, Props<T> props); the returned WidgetFunction<T> is the builder. Props<T> is a reactive view of the current widget instance — read props() or props.value to subscribe to widget-field changes from the parent.
class CounterCard extends SetupWidget<CounterCard> {
const CounterCard({
super.key,
required this.title,
required this.initialValue,
});
final String title;
final int initialValue;
@override
setup(context, props) {
final count = useSignal(props().initialValue);
final label = useComputed(() => '${props().title}: ${count.value}');
onMounted(() => debugPrint('mounted: ${props().title}'));
return () => GestureDetector(
onTap: () => count.value++,
child: Text(label.value),
);
}
}
Use SetupWidget when the component has a clear identity (constructor fields, public API) and parent updates to those fields should flow into setup-owned state through props.
SetupMixin<T> (on State<T>)
Mixes into an existing State<T> subclass. Override WidgetFunction<T> setup(BuildContext context); access widget fields via the mixin getter props (reactive) or widget (plain Flutter). All State lifecycle and instance methods remain available — the mixin only adds the setup runtime on top.
class _WelcomePanelState extends State<WelcomePanel>
with SetupMixin<WelcomePanel> {
late AnimationController controller;
void show() => controller.forward();
void hide() => controller.reverse();
@override
setup(context) {
controller = useAnimationController(
duration: const Duration(milliseconds: 250),
);
return () => Column(
mainAxisSize: MainAxisSize.min,
children: [
FadeTransition(opacity: controller, child: const FlutterLogo(size: 72)),
Row(children: [
FilledButton(onPressed: show, child: const Text('Show')),
OutlinedButton(onPressed: hide, child: const Text('Hide')),
]),
],
);
}
}
Use SetupMixin when you must keep a custom State<T> — e.g. to expose instance methods to a GlobalKey, mix in other State mixins, satisfy a base class, or migrate an existing widget incrementally.
Important: setup runs on first build, not in initState, because hooks may read inherited widgets (Theme.of(context) via useInherited). Override initState only for work that does not depend on setup-owned state.
SetupBuilder
A SetupWidget<SetupBuilder> constructed from an inline callback. No widget class needed; the setup callback receives BuildContext (no typed props, since the widget identity is SetupBuilder itself).
SetupBuilder(
setup: (context) {
final count = useSignal(0);
useEffect(() => debugPrint('count = ${count.value}'));
return () => FilledButton(
onPressed: () => count.value++,
child: Text('Count: ${count.value}'),
);
},
)
Use SetupBuilder for local composition, reactive leaf widgets in larger build methods, prototypes, or anywhere the cost of declaring a SetupWidget subclass outweighs its benefit. Each SetupBuilder in a parent build is a separate widget identity and gets its own setup runtime.
Hooks at a Glance
Hooks are top-level use* functions (or use*.variant(...) factories) that must be called inside a setup function. They fall into a few buckets:
- Reactive primitives —
useSignal(+.lazy/.list/.map/.set/.iterable/.async),useComputed(+.withPrevious/.writable/...),useEffect,usePostFrameEffect,useWatcher(+.immediately/.once),useEffectScope,useUntil(+.when/.changed). - Lifecycle callbacks —
onMounted,onUnmounted,onDidUpdateWidget,onDidChangeDependencies,onActivated,onDeactivated. - Context & memo —
useContext,useSetupContext,useInherited,useMemoized,useAutoDispose,useHook(+SetupHook<T>for custom hooks). - Flutter controllers — text/scroll/page/tab/animation/focus/etc. (auto-disposed).
- Listenable & sync bridges —
useValueNotifier,useChangeNotifier,useListen.{value,listenable,stream}(+.watch),useSync.{from,bidi}. - Async —
useFuture(+.watch),useStream(+.watch),useStreamController(+.broadcast). - Misc —
useTimer(+.periodic),useAppLifecycleState,useAutomaticKeepAlive(+.value), experimentaluseSetupResetfamily.
Full signatures, variants, lifecycle behavior, and examples are in references/hooks.md. To write your own hook — function-style, class-style (SetupHook<T>), or extension-style attached to a built-in creator like useSignal.myThing — see references/custom-hooks.md.
Rules
- Call
use*only at the top level ofsetup— never inside conditionals, loops, callbacks, or after early returns. The hook index must be stable across rebuilds. - Return only the
() => Widgetfromsetup. Don't return computed widgets directly; let the builder close over reactive state so rebuilds work. - Long-lived resources belong in hooks (
useSignal,useMemoized, controller hooks); they auto-dispose. Don'tnewthem insetupwithout a hook unless you also handle disposal viaonUnmountedor anEffectScope. - Use
props()(SetupWidget) or thepropsgetter (SetupMixin) when setup-owned state must react to parent updates; rawwidget.fooreads insetupare captured only at the first run. resetSetup()is a heavy escape hatch (rebuilds the whole hook sequence). Prefer ordinary signal updates.- Reading inherited widgets inside
setup: useuseInherited((c) => Theme.of(c))so dependency changes invalidate the cached value reactively. PlainTheme.of(context)works too but isn't reactive past the initial setup.
Source Navigation
Read source before answering API-sensitive questions.
| Area | Path |
|---|---|
| Public exports | packages/jolt_setup/lib/jolt_setup.dart |
SetupContext, SetupHook, lifecycle plumbing | packages/jolt_setup/lib/src/setup/framework.dart |
SetupWidget, SetupBuilder, SetupWidgetElement | packages/jolt_setup/lib/src/setup/widget.dart |
SetupMixin | packages/jolt_setup/lib/src/setup/stateful_mixin.dart |
| Built-in hooks | packages/jolt_setup/lib/src/hooks/ |
| Example | packages/jolt_setup/example/jolt_setup_example.dart |
| Tests | packages/jolt_setup/test/ |