asyncredux-optimistic-update-mixin
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.
Best use case
asyncredux-optimistic-update-mixin is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
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.
Teams using asyncredux-optimistic-update-mixin should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/asyncredux-optimistic-update-mixin/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How asyncredux-optimistic-update-mixin Compares
| Feature / Agent | asyncredux-optimistic-update-mixin | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
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.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Optimistic Update Mixins
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 |
## OptimisticCommand
Use for one-time server operations where immediate UI feedback matters: creating todos, deleting items, submitting forms, or processing payments.
### Basic Example
Without optimistic updates (user waits for server):
```dart
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):
```dart
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();
}
```
### How Rollback Works
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:
```dart
// Determine whether to restore previous state
bool shouldRollback() => true;
// Specify exact state to restore
AppState? rollbackState() => previousState;
```
### Non-Reentrant by Default
OptimisticCommand prevents concurrent execution of the same action. Use `nonReentrantKeyParams()` to allow parallel operations on different items:
```dart
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:
```dart
if (context.isWaiting(SaveTodo)) {
return CircularProgressIndicator();
}
```
### Combining with Other Mixins
- **With Retry**: Only `sendCommandToServer` retries; optimistic UI remains stable
- **With CheckInternet**: No optimistic state applied when offline
## OptimisticSync
Use for rapid user interactions (toggling likes, switches, sliders) where only the final value matters and intermediate states can be discarded.
### Toggle Example
```dart
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;
}
}
```
### How Coalescing Works
Multiple rapid changes are merged into minimal server requests:
1. User taps like button 5 times quickly
2. UI updates instantly each time (toggle, toggle, toggle...)
3. Only **one** server request sends the final state
4. If state changes during in-flight request, a follow-up request sends the new final value
## OptimisticSyncWithPush
Use when your app receives real-time server updates (WebSockets, Firebase) across multiple devices modifying shared data.
### Key Differences from OptimisticSync
- Each local dispatch increments a `localRevision` counter
- Server pushes do NOT increment `localRevision`
- Follow-up logic compares revisions instead of just values
- Stale pushes are automatically ignored
### Implementation
```dart
class 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;
}
}
```
### ServerPush Mixin
Handle incoming server pushes with automatic stale detection:
```dart
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.
### Data Model for Revision Tracking
Store server revisions in your data model:
```dart
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,
);
}
```
## Notifying Users of Rollback
To notify users when a rollback occurs, use `UserException` in your error handling:
```dart
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:
```dart
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;
}
```
## Choosing the Right Mixin
| 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` |
## References
URLs from the documentation:
- https://asyncredux.com/sitemap.xml
- https://asyncredux.com/flutter/advanced-actions/optimistic-mixins
- https://asyncredux.com/flutter/advanced-actions/action-mixins
- https://asyncredux.com/flutter/advanced-actions/before-and-after-the-reducer
- https://asyncredux.com/flutter/advanced-actions/control-mixins
- https://asyncredux.com/flutter/advanced-actions/errors-thrown-by-actions
- https://asyncredux.com/flutter/basics/async-actions
- https://asyncredux.com/flutter/basics/failed-actionsRelated Skills
asyncredux-wait-fail-succeed
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.
asyncredux-wait-condition
Use `waitCondition()` inside actions to pause execution until state meets criteria. Covers waiting for price thresholds, coordinating between actions, and implementing conditional workflows.
asyncredux-user-exceptions
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.
asyncredux-sync-actions
Creates AsyncRedux (Flutter) synchronous actions that update state immediately by implementing reduce() to return a new state.
asyncredux-streams-timers
Manage Streams and Timers with AsyncRedux. Covers creating actions to start/stop streams, storing stream subscriptions in store props, dispatching actions from stream callbacks, and proper cleanup with disposeProps().
asyncredux-state-access
Access store state in widgets using `context.state`, `context.select()`, and `context.read()`. Covers when to use each method, setting up BuildContext extensions, and optimizing widget rebuilds with selective state access.
asyncredux-setup
Initialize, setup and configure AsyncRedux in a Flutter app. Use it whenever starting a new AsyncRedux project, or when the user requests.
asyncredux-selectors
Create and cache selectors for efficient state access. Covers writing selector functions, caching with `cache1` and `cache2`, the reselect pattern, and avoiding repeated computations in widgets.
asyncredux-persistence
Implement local state persistence using Persistor. Covers creating a custom Persistor class, implementing `readState()`, `persistDifference()`, `deleteState()`, using LocalPersist helper, throttling saves, and pausing/resuming persistence with app lifecycle.
asyncredux-navigation
Handle navigation through actions using NavigateAction. Covers setting up the navigator key, dispatching NavigateAction for push/pop/replace, and testing navigation in isolation.
asyncredux-events
Use the Event class to interact with Flutter's stateful widgets (TextField, ListView, etc.). Covers creating Event objects in state, consuming events with `context.event()`, scrolling lists, changing text fields, and the event lifecycle.
asyncredux-dependency-injection
Inject dependencies into actions using the environment pattern. Covers creating an Environment class, passing it to the Store, accessing `env` from actions, and using dependency injection for testability.