Repository navigation
Flutter App Shell v2.0.6
Documentation
- π CRITICAL: Clarified correct pattern for reactive effects
- ALL signal writes inside effect() must use untracked()
- Applies to synchronous writes, not just async callbacks
- Prevents SignalEffectException when effects and Watch widgets coexist
- Added comprehensive examples in
docs/signals-best-practices.md - Added ReactiveUserService example in
packages/flutter_app_shell/example/lib/examples/reactive_service_example.dart - Updated
CLAUDE.mdState Management section with correct pattern - Enhanced documentation in
AppShellSettingsStore
What Changed from v2.0.5
v2.0.5 added the appReady signal to coordinate service initialization timing.
While useful, it didn't solve the core issue: improper use of Signals library.
The real fix is simpler: wrap ALL signal writes in effects with untracked().
Migration
If you're experiencing SignalEffectException in your app services:
Your effects likely have signal writes without untracked(). Update them:
// β BEFORE (causes SignalEffectException)
effect(() {
final data = sourceSignal.value;
targetSignal.value = processData(data); // Direct write
});
// β
AFTER (correct pattern)
effect(() {
final data = sourceSignal.value;
untracked(() {
targetSignal.value = processData(data); // Wrapped in untracked
});
});Real-world example from bug reports:
// β BEFORE - ChatManager with synchronous write
effect(() {
final chatDocs = chatsWatcher.value;
chats.value = chatDocs.map((doc) => Chat.fromJson(doc)).toList(); // Missing untracked!
});
// β
AFTER - Correct pattern
effect(() {
final chatDocs = chatsWatcher.value;
untracked(() {
chats.value = chatDocs.map((doc) => Chat.fromJson(doc)).toList();
});
});Why this works:
- Effect subscribes to
sourceSignalonly - Write to
targetSignaldoesn't create circular dependencies - Watch widgets can safely read
targetSignal - No SignalEffectException!
Files to check: - Any service with
effect()calls - ChatManager, EventFlowService, custom reactive services
- Look for signal writes inside effects (both sync and async)
TheappReadysignal from v2.0.5 is still useful for coordinating
service initialization, but it doesn't replace proper effect patterns.
Key Rule
From Signals documentation:
"Critical danger: Mutating a signal inside an effect causes infinite loops since the effect re-triggers. Use
untracked()to read without subscribing."
This applies to ALL signal writes, even when:
- β Writing to a different signal than you're reading
- β Writing synchronously (not in an async callback)
- β
The write seems unrelated to the trigger
Seedocs/signals-best-practices.mdfor comprehensive examples and patterns.