Spoosh
Plugins

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-invalidation

Note: This plugin requires @spoosh/plugin-invalidation as 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

PropertyTypeDefaultDescription
forapi.endpoint.$getrequiredThe endpoint to update
updater(data, response?) => datarequiredFunction to update cached data
match(request) => boolean-Filter which cache entries to update
timing"immediate" | "onSuccess""immediate"When to apply the update
rollbackOnErrorbooleantrueWhether to rollback on error
onError(error) => void-Error callback

Timing Modes

ModeDescription
"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:

PropertyTypeDescription
isOptimisticbooleantrue 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"],
});

On this page