楽観的更新とは
サーバーのレスポンスを待たずに、先に UI を更新する手法です。Server Action が完了したら実際のデータで上書きされ、失敗した場合は元の状態に自動で戻ります。
const [optimisticState, addOptimistic] = useOptimistic(
actualState, // 実際のデータ(Server Component から props で受け取る)
(state, newValue) => updatedState // 楽観的な状態を計算する関数
);
いいねボタンの例
"use client";
import { useOptimistic } from "react";
import { toggleLikeAction } from "./actions";
export function LikeButton({
postId,
initialCount,
initialLiked,
}: {
postId: string;
initialCount: number;
initialLiked: boolean;
}) {
const [optimistic, setOptimistic] = useOptimistic(
{ count: initialCount, liked: initialLiked },
(state) => ({ count: state.liked ? state.count - 1 : state.count + 1, liked: !state.liked })
);
async function handleClick() {
setOptimistic(undefined); // 楽観的に先に UI を更新
await toggleLikeAction(postId);
}
return (
<button onClick={handleClick}>
{optimistic.liked ? "❤️" : "🤍"} {optimistic.count}
</button>
);
}
リスト追加の例
"use client";
import { useOptimistic, useRef } from "react";
import { addCommentAction } from "./actions";
export function CommentForm({ postId, comments }: { postId: string; comments: Comment[] }) {
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(state, newComment: Comment) => [...state, newComment]
);
const ref = useRef<HTMLFormElement>(null);
async function handleSubmit(formData: FormData) {
const text = formData.get("text") as string;
addOptimisticComment({ id: crypto.randomUUID(), text, pending: true });
ref.current?.reset();
await addCommentAction(postId, text);
}
return (
<>
<ul>
{optimisticComments.map((c) => (
<li key={c.id} style={{ opacity: c.pending ? 0.5 : 1 }}>
{c.text}
</li>
))}
</ul>
<form ref={ref} action={handleSubmit}>
<input name="text" required />
<button type="submit">送信</button>
</form>
</>
);
}
失敗時の挙動
Server Action がエラーになると、楽観的に更新した UI は自動で元の状態(actualState)に戻ります。ただし、ユーザーへのエラー表示は別途実装が必要です。
async function handleSubmit(formData: FormData) {
addOptimisticItem(newItem); // 先に UI を更新
try {
await createItemAction(formData); // サーバー確認
} catch (e) {
// エラー時: useOptimistic の状態は自動ロールバックされる
// ユーザーへの通知は自前で行う
alert("失敗しました");
}
}
向いている場面 / 向かない場面
| 向いている | 向かない |
|---|---|
| いいね・フォロー(成功率が高い操作) | 決済・送金など失敗時のリカバリが複雑な操作 |
| コメント追加・削除 | サーバーが返す値に依存する操作(採番 ID など) |
| 表示非表示の切り替え | エラー時に丁寧なロールバック UI が必要な場面 |
送信状態の管理には → useActionState
Server Actions の使い方 → Server Actions