Optimistic Updates
Instant UI updates with automatic rollback on error
The optimistic plugin updates the UI immediately before the server responds. If the request fails, changes are automatically rolled back.
Installation
npm install @spoosh/plugin-optimistic @spoosh/plugin-invalidationNote: This plugin requires
@spoosh/plugin-invalidationas a peer dependency.
Usage
import { optimisticPlugin } from "@spoosh/plugin-optimistic";
import { invalidationPlugin } from "@spoosh/plugin-invalidation";
const plugins = [
cachePlugin({ staleTime: 5000 }),
deduplicationPlugin(),
invalidationPlugin(),
optimisticPlugin(),
];Basic Optimistic Update
const { trigger } = useWrite((api) => api.posts[id].$delete);
trigger({
optimistic: ($, api) =>
$({
for: api.posts.$get,
updater: (posts) => posts.filter((p) => p.id !== id),
rollbackOnError: true,
}),
});The updater function receives the current cached data and returns the optimistically updated data.
Update with Response Data
Use timing: "onSuccess" to update cache with the actual response:
const { trigger } = useWrite((api) => api.posts.$post);
trigger({
body: { title: "New Post", content: "..." },
optimistic: ($, api) =>
$({
for: api.posts.$get,
timing: "onSuccess",
updater: (posts, newPost) => [newPost!, ...posts],
}),
});Multiple Targets
Update multiple caches at once:
trigger({
optimistic: ($, api) => [
$({
for: api.posts.$get,
updater: (posts) => posts.filter((p) => p.id !== id),
}),
$({
for: api.stats.$get,
updater: (stats) => ({ ...stats, count: stats.count - 1 }),
}),
],
});Filter by Request Params
Only update specific cache entries:
trigger({
optimistic: ($, api) =>
$({
for: api.posts.$get,
match: (request) => request.query?.page === 1,
updater: (posts, newPost) => [newPost!, ...posts],
}),
});Options
Config Object
| Property | Type | Default | Description |
|---|---|---|---|
for | api.endpoint.$get | required | The endpoint to update |
updater | (data, response?) => data | required | Function to update cached data |
match | (request) => boolean | - | Filter which cache entries to update |
timing | "immediate" | "onSuccess" | "immediate" | When to apply the update |
rollbackOnError | boolean | true | Whether to rollback on error |
onError | (error) => void | - | Error callback |
Timing Modes
| Mode | Description |
|---|---|
"immediate" | Update cache instantly before request completes. Rollback on error. |
"onSuccess" | Wait for successful response, then update cache with response data. |
Result
The useRead hook returns an additional field when optimistic data is present:
| Property | Type | Description |
|---|---|---|
isOptimistic | boolean | true if current data is from an optimistic update |
function PostList() {
const { data, isOptimistic } = useRead((api) => api.posts.$get());
return (
<div>
{isOptimistic && <span>Saving...</span>}
<ul>
{data?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}Auto-Invalidation Behavior
By default, when using optimistic updates, autoInvalidate is set to "none" for that request to prevent immediate cache invalidation from overwriting your optimistic data.
If you want auto-invalidation to still happen, set it explicitly:
// Invalidate all related tags (full hierarchy)
trigger({
optimistic: ($, api) => $({ /* ... */ }),
autoInvalidate: "all",
});
// Invalidate only the exact endpoint tag
trigger({
optimistic: ($, api) => $({ /* ... */ }),
autoInvalidate: "self",
});
// Target specific endpoints or tags manually
trigger({
optimistic: ($, api) => $({ /* ... */ }),
autoInvalidate: "none",
invalidate: (api) => [api.posts.$get, api.stats.$get],
});
// Or use string tags
trigger({
optimistic: ($, api) => $({ /* ... */ }),
invalidate: ["posts", "dashboard"],
});