rtk-optimistic
v1.0.0
Published
Automatic optimistic updates with snapshot-based rollback for Redux Toolkit createAsyncThunk
Maintainers
Readme
rtk-optimistic
Automatic optimistic updates with snapshot-based rollback for Redux Toolkit's createAsyncThunk.
Stop hand-writing rollback logic for every optimistic update. rtk-optimistic snapshots your slice state right before the optimistic write, and automatically restores it if the underlying API call fails — no manual try/catch + revert code in every thunk.
Why
A common pattern with Redux Toolkit is:
// The "manual" way — every thunk repeats this dance
builder.addCase(updateTodo.pending, (state, action) => {
state.entities[action.meta.arg.id] = { ...state.entities[action.meta.arg.id], ...action.meta.arg.changes };
});
builder.addCase(updateTodo.rejected, (state, action) => {
// ...now you have to remember what it was before, and revert it by hand
});That revert logic gets duplicated (and often buggy or forgotten) across every optimistic thunk in a codebase. rtk-optimistic handles the snapshot + restore for you.
Install
npm install rtk-optimisticRequires @reduxjs/toolkit (^1.9.0 or ^2.0.0) as a peer dependency.
Usage
import { createOptimisticThunk } from "rtk-optimistic";
import { createSlice } from "@reduxjs/toolkit";
interface Todo {
id: string;
text: string;
done: boolean;
}
interface TodosState {
list: Todo[];
}
const toggleTodo = createOptimisticThunk<TodosState, { id: string; done: boolean }, Todo>({
typePrefix: "todos/toggle",
apiCall: (arg) => api.updateTodo(arg.id, { done: arg.done }),
optimisticUpdate: (state, arg) => {
const todo = state.list.find((t) => t.id === arg.id);
if (todo) todo.done = arg.done;
},
// optional: reconcile with the real server response on success
onSuccess: (state, serverTodo) => {
const todo = state.list.find((t) => t.id === serverTodo.id);
if (todo) Object.assign(todo, serverTodo);
},
});
const todosSlice = createSlice({
name: "todos",
initialState: { list: [] } as TodosState,
reducers: {},
extraReducers: (builder) => {
toggleTodo.attach(builder);
},
});
// Dispatch it like any other thunk:
dispatch(toggleTodo.thunk({ id: "1", done: true }));If api.updateTodo rejects, the slice state is automatically restored to exactly what it was before the optimistic update — no extra code needed.
API
createOptimisticThunk(config)
| Option | Required | Description |
|---|---|---|
| typePrefix | ✅ | Same as the first argument to createAsyncThunk. |
| apiCall | ✅ | Your async function (the real network request). |
| optimisticUpdate | ✅ | Mutates the draft state immediately, before the API call resolves. |
| onSuccess | – | Reconcile state with the real server response once resolved. |
| getSnapshot | – | Customize what gets snapshotted (default: full slice state via structuredClone). Useful for large slices where snapshotting everything is wasteful. |
| restoreSnapshot | – | Customize how a snapshot is restored on failure (default: Object.assign). |
Returns { thunk, attach }:
thunk— the underlyingcreateAsyncThunkaction creator. Dispatch it as usual.attach(builder)— call inside your slice'sextraReducersto register the pending/fulfilled/rejected cases.
How it works
- On dispatch (
pending) — a snapshot of the current slice state is stored (keyed by the thunk'srequestId, so concurrent calls don't clash), then youroptimisticUpdateis applied immediately. - On success (
fulfilled) — the snapshot is discarded; your optionalonSuccessruns to reconcile with the real server response. - On failure (
rejected) — the stored snapshot is restored into the draft automatically, undoing the optimistic write.
Works with plain object/array state shapes, including state managed by createEntityAdapter.
License
MIT
