.claude/skills/asyncredux-optimistic-update-mixin/SKILL.md
Add the OptimisticUpdate mixin for instant UI feedback before server confirmation. Covers immediate state changes, automatic rollback on failure, and optionally notifying users of rollback.
npx skillsauth add marcglasberg/async_redux asyncredux-optimistic-update-mixinInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
AsyncRedux provides three optimistic update mixins for different scenarios:
| Mixin | Use Case |
|-------|----------|
| OptimisticCommand | One-time operations (create, delete, submit) with rollback |
| OptimisticSync | Rapid toggling/interactions with coalescing |
| OptimisticSyncWithPush | Real-time server push scenarios with revision tracking |
Use for one-time server operations where immediate UI feedback matters: creating todos, deleting items, submitting forms, or processing payments.
Without optimistic updates (user waits for server):
class SaveTodo extends AppAction {
final Todo newTodo;
SaveTodo(this.newTodo);
Future<AppState?> reduce() async {
await saveTodo(newTodo);
var reloadedList = await loadTodoList();
return state.copy(todoList: reloadedList);
}
}
With OptimisticCommand (instant UI feedback):
class SaveTodo extends AppAction with OptimisticCommand {
final Todo newTodo;
SaveTodo(this.newTodo);
// Value to apply immediately to UI
Object? optimisticValue() => newTodo;
// Extract current value from state (for rollback comparison)
Object? getValueFromState(AppState state)
=> state.todoList.getById(newTodo.id);
// Apply value to state and return new state
AppState applyValueToState(AppState state, Object? value)
=> state.copy(todoList: state.todoList.add(value as Todo));
// Send to server (retries if using Retry mixin)
Future<Object?> sendCommandToServer(Object? value) async
=> await saveTodo(newTodo);
// Optional: reload from server on error
Future<Object?> reloadFromServer() async
=> await loadTodoList();
}
If sendCommandToServer fails, the mixin automatically rolls back only if the current state still matches the optimistic value. This avoids undoing newer changes made while the request was in flight.
Override these methods to customize rollback:
// Determine whether to restore previous state
bool shouldRollback() => true;
// Specify exact state to restore
AppState? rollbackState() => previousState;
OptimisticCommand prevents concurrent execution of the same action. Use nonReentrantKeyParams() to allow parallel operations on different items:
class SaveTodo extends AppAction with OptimisticCommand {
final String itemId;
SaveTodo(this.itemId);
// Allow SaveTodo('A') and SaveTodo('B') to run simultaneously
// but prevent two SaveTodo('A') from running together
Object? nonReentrantKeyParams() => itemId;
// ... rest of implementation
}
Check if action is in progress in UI:
if (context.isWaiting(SaveTodo)) {
return CircularProgressIndicator();
}
sendCommandToServer retries; optimistic UI remains stableUse for rapid user interactions (toggling likes, switches, sliders) where only the final value matters and intermediate states can be discarded.
class ToggleLike extends AppAction with OptimisticSync<AppState, bool> {
final String itemId;
ToggleLike(this.itemId);
// Allow concurrent operations on different items
Object? optimisticSyncKeyParams() => itemId;
// Value to apply optimistically (toggle current value)
bool valueToApply() => !state.items[itemId].liked;
// Apply optimistic change to state
AppState applyOptimisticValueToState(AppState state, bool isLiked)
=> state.copy(items: state.items.setLiked(itemId, isLiked));
// Extract current value from state
bool getValueFromState(AppState state) => state.items[itemId].liked;
// Send to server
Future<Object?> sendValueToServer(Object? value) async
=> await api.setLiked(itemId, value);
// Optional: Apply server response to state
AppState? applyServerResponseToState(AppState state, Object serverResponse)
=> state.copy(items: state.items.setLiked(itemId, serverResponse as bool));
// Optional: Handle completion/errors
Future<AppState?> onFinish(Object? error) async {
if (error != null) {
// Reload from server on failure
var reloaded = await api.getItem(itemId);
return state.copy(items: state.items.update(itemId, reloaded));
}
return null;
}
}
Multiple rapid changes are merged into minimal server requests:
Use when your app receives real-time server updates (WebSockets, Firebase) across multiple devices modifying shared data.
localRevision counterlocalRevisionclass ToggleLike extends AppAction with OptimisticSyncWithPush<AppState, bool> {
final String itemId;
ToggleLike(this.itemId);
Object? optimisticSyncKeyParams() => itemId;
bool valueToApply() => !state.items[itemId].liked;
AppState applyOptimisticValueToState(AppState state, bool isLiked)
=> state.copy(items: state.items.setLiked(itemId, isLiked));
bool getValueFromState(AppState state) => state.items[itemId].liked;
// Read server revision from state
int? getServerRevisionFromState(Object? key)
=> state.items[key as String].serverRevision;
AppState? applyServerResponseToState(AppState state, Object serverResponse)
=> state.copy(items: state.items.setLiked(itemId, serverResponse as bool));
Future<Object?> sendValueToServer(Object? value) async {
// Get local revision BEFORE await
int localRev = localRevision();
var response = await api.setLiked(itemId, value, localRev: localRev);
// Record server's revision after response
informServerRevision(response.serverRev);
return response.liked;
}
}
Handle incoming server pushes with automatic stale detection:
class PushLikeUpdate extends AppAction with ServerPush<AppState> {
final String itemId;
final bool liked;
final int serverRev;
PushLikeUpdate({
required this.itemId,
required this.liked,
required this.serverRev,
});
// Link to corresponding OptimisticSyncWithPush action
Type associatedAction() => ToggleLike;
Object? optimisticSyncKeyParams() => itemId;
int serverRevision() => serverRev;
int? getServerRevisionFromState(Object? key)
=> state.items[key as String].serverRevision;
AppState? applyServerPushToState(AppState state, Object? key, int serverRevision)
=> state.copy(
items: state.items.update(
key as String,
(item) => item.copy(liked: liked, serverRevision: serverRevision),
),
);
}
If incoming serverRevision ≤ current known revision, the push is automatically ignored. This prevents older server states from overwriting newer ones.
Store server revisions in your data model:
class Item {
final bool liked;
final int? serverRevision;
Item({required this.liked, this.serverRevision});
Item copy({bool? liked, int? serverRevision}) => Item(
liked: liked ?? this.liked,
serverRevision: serverRevision ?? this.serverRevision,
);
}
To notify users when a rollback occurs, use UserException in your error handling:
class SaveTodo extends AppAction with OptimisticCommand {
// ... required methods ...
Future<Object?> sendCommandToServer(Object? value) async {
try {
return await saveTodo(newTodo);
} catch (e) {
// Throw UserException to show dialog after rollback
throw UserException('Failed to save. Your change was reverted.').addCause(e);
}
}
}
Or use onFinish with OptimisticSync:
Future<AppState?> onFinish(Object? error) async {
if (error != null) {
// Dispatch a notification action
dispatch(UserExceptionAction('Failed to update. Reverting...'));
// Reload correct state from server
var reloaded = await api.getItem(itemId);
return state.copy(items: state.items.update(itemId, reloaded));
}
return null;
}
| Scenario | Mixin |
|----------|-------|
| Create/delete/submit operations | OptimisticCommand |
| Toggle switches, like buttons | OptimisticSync |
| Sliders, rapid input changes | OptimisticSync |
| Multi-device with real-time sync | OptimisticSyncWithPush + ServerPush |
URLs from the documentation:
data-ai
Show loading states and handle action failures in widgets. Covers `isWaiting(ActionType)` for spinners, `isFailed(ActionType)` for error states, `exceptionFor(ActionType)` for error messages, and `clearExceptionFor()` to reset failure states.
data-ai
Use `waitCondition()` inside actions to pause execution until state meets criteria. Covers waiting for price thresholds, coordinating between actions, and implementing conditional workflows.
testing
Handle user-facing errors with UserException. Covers throwing UserException from actions, setting up UserExceptionDialog, customizing error dialogs with `onShowUserExceptionDialog`, and using UserExceptionAction for non-interrupting error display.
tools
Implement undo/redo functionality using state observers. Covers recording state history with stateObserver, creating a RecoverStateAction, implementing undo for the full state or partial state, and managing history limits.