Optimistic Updates
Optimistic updates are a powerful UI pattern that makes your application feel significantly faster. The idea is to update the UI immediately after a user action, assuming the server request will succeed. If the request later fails, the changes are automatically rolled back. This avoids making the user wait for the network round-trip.
HyperFetch provides first-class support for optimistic updates through the request.setOptimistic() API. You define
cache manipulation, rollback logic, and invalidation targets directly on the request — the framework orchestrates
everything automatically, whether you use send(), $hooks, or React hooks.
- How to configure optimistic updates with
request.setOptimistic(). - How
rollbackrestores the cache on failure automatically. - How
invalidatekeeps your data in sync on success. - How
mutationContextlets you access snapshot data in lifecycle callbacks.
Basic Example: Updating a User
The most common optimistic pattern is updating a list item. The user clicks "Save," the list updates instantly, and the request is sent in the background.
1. Define the request with setOptimistic
import { client } from "./client";
interface User {
id: number;
name: string;
email: string;
}
const getUsers = client.createRequest<{ response: User[] }>()({
endpoint: "/users",
method: "GET",
});
const patchUser = client
.createRequest<{
response: User;
payload: Partial<User>;
}>()({
endpoint: "/users/:userId",
method: "PATCH",
})
.setOptimistic(({ client, payload, request }) => {
// 1. Snapshot the current cache
const snapshot = client.cache.get(getUsers.cacheKey);
// 2. Optimistically update the cache
client.cache.update(getUsers, (prev) => ({
...prev,
data: prev.data.map((u) => (u.id === Number(request.params?.userId) ? { ...u, ...payload } : u)),
}));
return {
// Available as `mutationContext` in callbacks
context: { snapshot },
// Called automatically on failure or abort
rollback: () => {
if (snapshot) client.cache.set(getUsers, snapshot);
},
// Cache keys to invalidate on success
invalidate: [getUsers],
};
});
2. Use in a React component
import { useFetch, useSubmit } from "@hyper-fetch/react";
function UserList() {
const { data: users } = useFetch(getUsers);
const { submit, onSubmitSuccess, onSubmitError } = useSubmit(patchUser);
// mutationContext is auto-inferred — no extra generics needed
onSubmitSuccess(({ mutationContext }) => {
console.log("Updated! Previous snapshot:", mutationContext?.snapshot);
});
onSubmitError(({ mutationContext }) => {
// rollback already happened automatically
alert("Update failed — changes have been reverted.");
});
const handleRename = (userId: number, newName: string) => {
submit({
params: { userId },
payload: { name: newName },
});
};
return (
<ul>
{users?.map((user) => (
<li key={user.id}>
{user.name}
<button onClick={() => handleRename(user.id, "New Name")}>Rename</button>
</li>
))}
</ul>
);
}
How It Works
- Before the request is sent,
setOptimisticruns: it snapshots the cache, patches it optimistically, and returnscontext,rollback, andinvalidate. - The UI updates immediately because
useFetch(getUsers)reacts to cache changes. - On success: the
invalidatelist triggers a cache refresh forgetUsers, andonSubmitSuccessreceives the typedmutationContext. - On failure:
rollback()is called automatically to restore the cache, thenonSubmitErrorfires withmutationContext. - On abort: treated like failure —
rollback()runs, thenonSubmitAbortfires.
The setOptimistic Callback
The callback receives an object with:
| Argument | Description |
|---|---|
request | The request instance (with params, endpoint, etc.) |
client | The client instance (access to client.cache) |
payload | The payload being sent (typed from the request generic) |
It returns an object with three optional fields:
| Return field | Type | Description |
|---|---|---|
context | Ctx (generic) | Passed as mutationContext in all lifecycle callbacks |
rollback | () => void | Called automatically on failure or abort |
invalidate | RequestInstance[] | Cache keys invalidated on success |
Advanced: Custom Rollback for Creates
When creating a new item (not updating an existing one), the rollback might need to remove the optimistically added entry:
const createUser = client
.createRequest<{
response: User;
payload: { name: string; email: string };
}>()({
endpoint: "/users",
method: "POST",
})
.setOptimistic(({ client, payload }) => {
const tempId = -Date.now();
const optimisticUser: User = { id: tempId, ...payload };
client.cache.update(getUsers, (prev) => ({
...prev,
data: [...prev.data, optimisticUser],
}));
return {
context: { tempId },
rollback: () => {
client.cache.update(getUsers, (prev) => ({
...prev,
data: prev.data.filter((u) => u.id !== tempId),
}));
},
invalidate: [getUsers],
};
});
Cautions
cache.updatevscache.set: For creates (adding new items), usecache.updateto merge into the existing list.cache.setreplaces the entire cache entry.- Concurrent same-key mutations: If two optimistic updates target the same cache key concurrently, each has its own
rollback. A failure on one will only revert to its snapshot, which may not include the other's changes. See the Advanced Optimistic Patterns guide for per-item rollback strategies that avoid this problem.
- Use
request.setOptimistic()to define optimistic behavior on the request itself — no hook configuration needed. - Return
contextfor typed data available in all lifecycle callbacks asmutationContext. - Return
rollbackto automatically revert cache changes on failure or abort. - Return
invalidateto refresh stale cache entries on success. mutationContextis auto-inferred from thesetOptimisticreturn type — no extra generics onuseSubmit.
Next Steps
For concurrent mutations, per-item rollback, toggle patterns, and cross-cache coordination, see the Advanced Optimistic Patterns guide.
