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 { Spoosh } from "@spoosh/core";
import { optimisticPlugin } from "@spoosh/plugin-optimistic";
import { invalidationPlugin } from "@spoosh/plugin-invalidation";
const client = new Spoosh<ApiSchema, Error>("/api").use([
cachePlugin({ staleTime: 5000 }),
deduplicationPlugin(),
invalidationPlugin(),
optimisticPlugin(),
]);Basic Optimistic Update
Use the chained DSL to define optimistic updates:
deletePost = injectWrite((api) => api("posts/:id").DELETE);
deletePost.trigger({
params: { id },
optimistic: (api) =>
api("posts")
.GET()
.UPDATE_CACHE((posts) => posts.filter((p) => p.id !== id)),
});The UPDATE_CACHE function receives the current cached data and returns the optimistically updated data.
Update with Response Data
Use ON_SUCCESS to update cache with the actual response:
createPost = injectWrite((api) => api("posts").POST);
createPost.trigger({
body: { title: "New Post", content: "..." },
optimistic: (api) =>
api("posts")
.GET()
.ON_SUCCESS()
.UPDATE_CACHE((posts, newPost) => [newPost!, ...posts]),
});Multiple Targets
Update multiple caches at once by returning an array:
deletePost.trigger({
optimistic: (api) => [
api("posts")
.GET()
.UPDATE_CACHE((posts) => posts.filter((p) => p.id !== id)),
api("stats")
.GET()
.UPDATE_CACHE((stats) => ({ ...stats, count: stats.count - 1 })),
],
});Filter by Request Params
Use WHERE to only update specific cache entries:
createPost.trigger({
optimistic: (api) =>
api("posts")
.GET()
.WHERE((request) => request.query?.page === 1)
.UPDATE_CACHE((posts, newPost) => [newPost!, ...posts]),
});Options
Per-Request Options
| Option | Type | Description |
|---|---|---|
optimistic | (api) => builder | builder[] | Callback to define optimistic updates |
Builder Methods (DSL)
Chain methods to configure optimistic updates:
| Method | Description |
|---|---|
.GET() | Select the GET endpoint to update |
.WHERE(fn) | Filter which cache entries to update |
.UPDATE_CACHE(fn) | Update cache immediately (default timing) |
.ON_SUCCESS() | Switch to onSuccess timing mode |
.NO_ROLLBACK() | Disable automatic rollback on error |
.ON_ERROR(fn) | Error callback |
Timing Modes
| Usage | Description |
|---|---|
.UPDATE_CACHE(fn) | Immediate - Update cache instantly before request completes. Rollback on error. |
.ON_SUCCESS().UPDATE_CACHE(fn) | On Success - Wait for successful response, then update cache. fn receives response as 2nd arg. |
Result
The injectRead function returns plugin metadata via the meta property:
| Property | Type | Description |
|---|---|---|
meta.isOptimistic | boolean | true if current data is from an optimistic update |
@Component({
selector: "app-post-list",
template: `
@if (posts.meta().isOptimistic) {
<span>Saving...</span>
}
<ul>
@for (post of posts.data(); track post.id) {
<li>{{ post.title }}</li>
}
</ul>
`,
})
export class PostListComponent {
posts = injectRead((api) => api("posts").GET());
}Auto-Invalidation Behavior
By default, when using optimistic updates, invalidation mode 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:
deletePost = injectWrite((api) => api("posts/:id").DELETE);
// Invalidate all related tags (full hierarchy)
deletePost.trigger({
optimistic: (api) =>
api("posts")
.GET()
.UPDATE_CACHE((posts) => [...posts]),
invalidate: "all",
});
// Invalidate only the exact endpoint tag
deletePost.trigger({
optimistic: (api) =>
api("posts")
.GET()
.UPDATE_CACHE((posts) => [...posts]),
invalidate: "self",
});
// Target specific tags manually
deletePost.trigger({
optimistic: (api) =>
api("posts")
.GET()
.UPDATE_CACHE((posts) => [...posts]),
invalidate: ["posts", "stats", "dashboard"],
});