使用 useOptimistic 提供即时反馈
为点赞、评论和切换操作应用乐观界面更新,同时让操作在后台完成。
使用 useOptimistic 提供即时反馈 是 CoddyKit 上的免费 Next.js 15 Fullstack (App Router + Server Actions) 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Next.js 15 Fullstack (App Router + Server Actions) 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Next.js 15 Fullstack (App Router + Server Actions) 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
The Latency Problem
When a user clicks a like button, the change has to travel to the server and back before the UI reflects it. On a slow network that delay feels broken.
The fix is optimistic UI: assume the action will succeed, update the screen instantly, and reconcile with the server once it responds. If the server fails, you roll back automatically.
React 19 (shipped with Next.js 15) gives you a dedicated hook for this: useOptimistic.
What useOptimistic Returns
useOptimistic takes your real state and an update function, and returns a temporary optimistic value plus a function to apply optimistic changes.
optimisticState— what you render right nowaddOptimistic(value)— queue an optimistic change
The signature is useOptimistic(state, (currentState, optimisticValue) => newState). The reducer-style function merges the optimistic value into the current state.
const [optimisticState, addOptimistic] = useOptimistic(
state,
(currentState, optimisticValue) => {
// return the new state to display optimistically
return { ...currentState, ...optimisticValue };
}
);A Like Button: The Server Action
First, the real work happens in a Server Action. It runs on the server, mutates the database, and revalidates the cache.
Notice the "use server" directive and the revalidatePath call so other parts of the app see the fresh count.
"use server";
import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";
export async function toggleLike(postId: string, liked: boolean) {
await db.post.update({
where: { id: postId },
data: { likes: { increment: liked ? 1 : -1 } },
});
revalidatePath("/feed");
}Wiring useOptimistic to the Button
In a Client Component, we hold the optimistic like state. When clicked, we call addOptimistic first (instant UI), then await the Server Action.
If the action throws, React automatically discards the optimistic value and reverts to the real likes prop.
"use client";
import { useOptimistic, useTransition } from "react";
import { toggleLike } from "./actions";
export function LikeButton({ postId, likes, liked }: {
postId: string; likes: number; liked: boolean;
}) {
const [optimistic, addOptimistic] = useOptimistic(
{ likes, liked },
(state, next: boolean) => ({
liked: next,
likes: state.likes + (next ? 1 : -1),
})
);
const [, startTransition] = useTransition();
return (
<button
onClick={() =>
startTransition(async () => {
addOptimistic(!optimistic.liked);
await toggleLike(postId, !optimistic.liked);
})
}
>
{optimistic.liked ? "❤️" : "🤍"} {optimistic.likes}
</button>
);
}Why You Need a Transition
Optimistic updates must happen inside a transition. Calling addOptimistic outside one throws an error.
- Use
useTransitionfor button clicks, or - Use
<form action={...}>— form actions are wrapped in a transition automatically
The transition tells React the update is non-urgent and tied to a pending async action. When the action finishes (and revalidation arrives), the optimistic value is replaced by the real one.
Optimistic Comments with a Form
For adding a comment, the form's action already runs in a transition, so you can call addOptimistic directly inside it.
We append a temporary comment (with a sending flag) so the user sees it immediately, then submit to the server.
"use client";
import { useOptimistic, useRef } from "react";
import { addComment } from "./actions";
type Comment = { id: string; text: string; sending?: boolean };
export function Comments({ postId, comments }: {
postId: string; comments: Comment[];
}) {
const formRef = useRef<HTMLFormElement>(null);
const [optimistic, addOptimistic] = useOptimistic(
comments,
(state, text: string) => [
...state,
{ id: crypto.randomUUID(), text, sending: true },
]
);
return (
<>
<ul>
{optimistic.map((c) => (
<li key={c.id} style={{ opacity: c.sending ? 0.5 : 1 }}>
{c.text}
</li>
))}
</ul>
<form
ref={formRef}
action={async (formData) => {
const text = formData.get("text") as string;
addOptimistic(text);
formRef.current?.reset();
await addComment(postId, text);
}}
>
<input name="text" />
<button type="submit">Post</button>
</form>
</>
);
}The Mental Model: Snapshots, Not Mutations
useOptimistic does not change your real state. It layers a temporary view on top.
- The base state is your prop/state from the server.
- The optimistic value lives only while a transition is pending.
- When the transition settles, React throws the optimistic value away and re-renders from the (now updated) base state.
This is why rollback is free: there is nothing to undo, you simply stop showing the optimistic layer.
A Reusable Toggle Reducer
The update function is just a pure reducer. You can model any toggle — likes, bookmarks, follow/unfollow — by switching a boolean and deriving counts.
Because it is pure, you can unit-test it without React. Here is a standalone reducer that mirrors what we pass to useOptimistic.
type ToggleState = { active: boolean; count: number };
function toggleReducer(state: ToggleState, next: boolean): ToggleState {
return {
active: next,
count: state.count + (next ? 1 : -1),
};
}
const start: ToggleState = { active: false, count: 10 };
const afterLike = toggleReducer(start, true);
const afterUnlike = toggleReducer(afterLike, false);
console.log(afterLike); // { active: true, count: 11 }
console.log(afterUnlike); // { active: false, count: 10 }Handling Failures Gracefully
If the Server Action rejects, React reverts the optimistic state, but the user should know why. Wrap the action in try/catch and surface an error (a toast, an inline message).
The optimistic value disappears automatically; your job is only to communicate the failure.
startTransition(async () => {
addOptimistic(!optimistic.liked);
try {
await toggleLike(postId, !optimistic.liked);
} catch (err) {
// optimistic value already reverted by React
toast.error("Could not save your like. Try again.");
}
});Keep the Base State in Sync
For automatic reconciliation to work, the base state must come from the server and refresh after the mutation. Two common ways:
revalidatePath/revalidateTaginside the Server Action re-fetches the page data.- Passing fresh props down to the Client Component (e.g. from a Server Component parent).
If the base state never updates, the optimistic value will vanish and snap back to the old count — a classic flicker bug.
useOptimistic vs useState
You could fake optimism with useState, but you would have to manually save the previous value, write rollback logic, and clear it after the request. That is error-prone.
useOptimisticties the optimistic value to the transition lifecycle — auto-applied while pending, auto-discarded when settled.- It re-syncs whenever the base state changes, so server truth always wins.
Reach for useOptimistic whenever an action has a clear, predictable success outcome you can show immediately.
Quick Check
Test your understanding of how useOptimistic behaves.
Recap
You learned how to deliver instant feedback with useOptimistic in Next.js 15:
- useOptimistic(state, reducer) returns an optimistic view plus an
addOptimisticfunction. - Optimistic updates must run inside a transition (via
useTransitionor a formaction). - The real mutation lives in a Server Action that calls
revalidatePath/revalidateTagso the base state refreshes. - On success the optimistic layer is replaced by server truth; on failure React auto-reverts — you just surface an error.
- Prefer it over manual
useStaterollback for likes, comments, and toggles.
常见问题解答
「使用 useOptimistic 提供即时反馈」课时是免费的吗?
是的 — 「使用 useOptimistic 提供即时反馈」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Next.js 15 Fullstack (App Router + Server Actions) 课程的其余内容,请升级到 CoddyKit PRO。 Next.js 15 Fullstack (App Router + Server Actions) 课程共包含 4 节课。
「使用 useOptimistic 提供即时反馈」这节课中我会学到什么?
为点赞、评论和切换操作应用乐观界面更新,同时让操作在后台完成。 你通过在浏览器中直接运行的动手代码来练习 Next.js 15 Fullstack (App Router + Server Actions),全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Next.js 15 Fullstack (App Router + Server Actions) 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Next.js 15 Fullstack (App Router + Server Actions) 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「使用 useOptimistic 提供即时反馈」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Next.js 15 Fullstack (App Router + Server Actions) 课中编写并运行代码吗?
能。每节 Next.js 15 Fullstack (App Router + Server Actions) 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 使用表单 Action 属性实现渐进式增强
- 使用 useFormStatus 处理待提交与加载状态
- 使用 useActionState 处理字段级验证错误
- 使用 useOptimistic 提供即时反馈