Add optimisticCommand for blocking one-time actions with immediate UI updates and automatic rollback on failure
This skill adds optimisticCommand for one-time actions with immediate UI updates and automatic rollback on failure.
Implements optimistic updates for blocking, one-time operations like:
The UI updates immediately, the server operation runs, and on failure the UI rolls back.
Use optimisticCommand when:
import 'package:bloc_superpowers/bloc_superpowers.dart';
class TodoCubit extends Cubit<TodoState> {
TodoCubit() : super(const TodoState());
void addTodo(Todo newTodo) => optimisticCommand(
key: (AddTodo, newTodo.id),
// 1. Return the optimistic value (new list with item added)
optimisticValue: () => state.todoList.add(newTodo),
// 2. Extract the value from current state
getValueFromState: (state) => state.todoList,
// 3. Apply a value back to state
applyValueToState: (state, value) =>
state.copyWith(todoList: value as List<Todo>),
// 4. Send the command to the server
sendCommandToServer: (optimisticValue) async {
await api.saveTodo(newTodo);
return null; // Return server response if needed
},
);
}
Rollback Safety: Rollback only occurs if the current state value still matches the optimistic value. This prevents overwriting concurrent changes made by other operations. If another update changed the value while the command was running, rollback is skipped to preserve that newer change.
| Parameter | Purpose |
|-----------|---------|
| key | Identifies the operation for state tracking and non-reentrant |
| optimisticValue | Returns the immediate UI value |
| getValueFromState | Extracts the relevant value from state |
| applyValueToState | Applies a value back to state |
| sendCommandToServer | Executes the server operation |
When the server returns data that should update the state:
void addTodo(Todo newTodo) => optimisticCommand(
key: (AddTodo, newTodo.id),
optimisticValue: () => state.todoList.add(newTodo),
getValueFromState: (state) => state.todoList,
applyValueToState: (state, value) =>
state.copyWith(todoList: value as List<Todo>),
sendCommandToServer: (optimisticValue) async {
final savedTodo = await api.saveTodo(newTodo);
return savedTodo; // Return server response
},
// Replace client-side todo with server-side todo
applyServerResponseToState: (state, serverResponse) {
final savedTodo = serverResponse as Todo;
return state.copyWith(
todoList: state.todoList
.where((t) => t.id != newTodo.id)
.toList()
..add(savedTodo),
);
},
);
Reload data after success or failure:
void deleteTodo(String todoId) => optimisticCommand(
key: (DeleteTodo, todoId),
optimisticValue: () =>
state.todoList.where((t) => t.id != todoId).toList(),
getValueFromState: (state) => state.todoList,
applyValueToState: (state, value) =>
state.copyWith(todoList: value as List<Todo>),
sendCommandToServer: (optimisticValue) async {
await api.deleteTodo(todoId);
return null;
},
// Reload after operation
reloadFromServer: () async {
return await api.loadTodoList();
},
// Control when to reload
shouldReload: ({
required currentValue,
required lastAppliedValue,
required optimisticValue,
required rollbackValue,
required error,
}) => error != null, // Only reload on failure
);
Customize how rollback works:
void addTodo(Todo newTodo) => optimisticCommand(
key: (AddTodo, newTodo.id),
// ... required params ...
// Control whether to rollback
shouldRollback: ({
required currentValue,
required initialValue,
required optimisticValue,
required error,
}) => true, // Default: always rollback on error
// Custom rollback state (e.g., show failed status instead of removing)
rollbackState: ({
required state,
required initialValue,
required optimisticValue,
required error,
}) => state.copyWith(
todoList: state.todoList.map((t) =>
t.id == newTodo.id ? t.copyWith(status: TodoStatus.failed) : t
).toList(),
),
);
Track loading state globally but prevent duplicates per item:
void addTodo(Todo newTodo) => optimisticCommand(
key: AddTodo, // isWaiting(AddTodo) shows any add in progress
nonReentrantKey: (AddTodo, newTodo.id), // Prevents duplicate for same todo
// ... rest of params ...
);
// Widget
if (context.isWaiting(AddTodo)) {
// Any add operation is in progress
}
void addItem(Item newItem) => optimisticCommand(
key: (AddItem, newItem.id),
optimisticValue: () => [...state.items, newItem],
getValueFromState: (state) => state.items,
applyValueToState: (state, value) =>
state.copyWith(items: value as List<Item>),
sendCommandToServer: (optimisticValue) async {
await api.createItem(newItem);
return null;
},
);
void deleteItem(String itemId) => optimisticCommand(
key: (DeleteItem, itemId),
optimisticValue: () =>
state.items.where((i) => i.id != itemId).toList(),
getValueFromState: (state) => state.items,
applyValueToState: (state, value) =>
state.copyWith(items: value as List<Item>),
sendCommandToServer: (optimisticValue) async {
await api.deleteItem(itemId);
return null;
},
);
void updateItem(Item updatedItem) => optimisticCommand(
key: (UpdateItem, updatedItem.id),
optimisticValue: () => state.items
.map((i) => i.id == updatedItem.id ? updatedItem : i)
.toList(),
getValueFromState: (state) => state.items,
applyValueToState: (state, value) =>
state.copyWith(items: value as List<Item>),
sendCommandToServer: (optimisticValue) async {
await api.updateItem(updatedItem);
return null;
},
);
Widget build(BuildContext context) {
final isSaving = context.isWaiting((AddTodo, todoId));
return ListTile(
title: Text(todo.title),
trailing: isSaving
? const CircularProgressIndicator()
: const Icon(Icons.check),
);
}
| Scenario | Use |
|----------|-----|
| Add/Delete/Update item | optimisticCommand |
| Form submission | optimisticCommand |
| Toggle (like/favorite) with fast taps | optimisticSync |
| Slider/rating value | optimisticSync |
| Settings that change rapidly | optimisticSync |
Rule: Use optimisticCommand for discrete actions, optimisticSync for values that may change rapidly.
Ask the user:
applyServerResponseToState)reloadFromServer)rollbackState)npx skills add marcglasberg/add-optimistic-command下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer