Appearance
useOptimistic乐观更新
学习目标
通过本章学习,你将掌握:
- useOptimistic的核心概念
- 乐观更新的工作原理
- 基本使用方法
- UI即时反馈
- 错误回滚机制
- 与Server Actions集成
- 实际应用场景
- 最佳实践
第一部分:乐观更新概念
1.1 什么是乐观更新
乐观更新是一种UI模式:在服务器响应前,先乐观地更新UI,假设操作会成功。如果失败,再回滚到原始状态。
jsx
// ========== 传统方式(悲观更新) ==========
function TraditionalLike({ postId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const [loading, setLoading] = useState(false);
const handleLike = async () => {
setLoading(true);
try {
const response = await fetch(`/api/posts/${postId}/like`, {
method: 'POST'
});
const data = await response.json();
setLikes(data.likes); // 等待服务器响应
} catch (error) {
alert('点赞失败');
} finally {
setLoading(false);
}
};
return (
<button onClick={handleLike} disabled={loading}>
{loading ? '...' : `❤️ ${likes}`}
</button>
);
}
// 问题:
// - 点击后需要等待服务器响应
// - 有明显的延迟感
// - 用户体验不够流畅
// ========== 乐观更新方式 ==========
'use client';
import { useOptimistic } from 'react';
import { likePost } from './actions';
function OptimisticLike({ postId, initialLikes }) {
const [optimisticLikes, setOptimisticLikes] = useOptimistic(
initialLikes,
(currentLikes, newLikes) => newLikes
);
const handleLike = async () => {
// 立即更新UI
setOptimisticLikes(initialLikes + 1);
// 后台发送请求
await likePost(postId);
};
return (
<button onClick={handleLike}>
❤️ {optimisticLikes}
</button>
);
}
// 优势:
// - 点击后立即响应
// - 无需等待服务器
// - 用户体验流畅
// - 失败时自动回滚1.2 工作原理
乐观更新流程:
1. 用户操作(点击按钮)
↓
2. 立即更新UI(乐观状态)
↓
3. 发起服务器请求
↓
4. 等待服务器响应
↓
5a. 成功 → 保持乐观状态
OR
5b. 失败 → 回滚到原始状态
详细流程图:
用户点击
↓
调用setOptimisticState(newValue)
↓
React立即更新optimisticState
↓
UI立即重新渲染
↓
发起异步请求(在后台)
↓
等待响应(用户看到乐观状态)
↓
┌─────────────┬─────────────┐
│ 请求成功 │ 请求失败 │
└─────────────┴─────────────┘
↓ ↓
更新actualState actualState不变
↓ ↓
optimisticState optimisticState
= actualState 自动回滚到actualState
↓ ↓
完成 显示错误1.3 基本语法
typescript
const [optimisticState, setOptimisticState] = useOptimistic(
actualState, // 实际状态(来自服务器)
(currentState, optimisticValue) => { // 更新函数
return optimisticValue;
}
);
// 参数:
// - actualState: 实际的状态值
// - updateFn: 接收当前状态和乐观值,返回新的乐观状态
// 返回值:
// - optimisticState: 当前显示的状态(可能是乐观值)
// - setOptimisticState: 设置乐观状态的函数
// TypeScript类型定义
function useOptimistic<State, Action>(
passthrough: State,
reducer: (state: State, action: Action) => State
): [State, (action: Action) => void];
// 示例
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos, // 实际的todos列表
(state: Todo[], newTodo: Todo) => [...state, newTodo]
);1.4 与传统方式的对比
jsx
// ========== 方式1:传统加载状态 ==========
function TraditionalApproach() {
const [count, setCount] = useState(0);
const [loading, setLoading] = useState(false);
const increment = async () => {
setLoading(true);
try {
const result = await updateCount(count + 1);
setCount(result);
} finally {
setLoading(false);
}
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment} disabled={loading}>
{loading ? 'Loading...' : '+1'}
</button>
</div>
);
}
// 时间线:
// 0ms: 用户点击
// 0ms: 显示Loading...
// 500ms: 服务器响应
// 500ms: 显示新数字
// 体验:500ms的等待时间
// ========== 方式2:乐观更新 ==========
function OptimisticApproach() {
const [count, setCount] = useState(0);
const [optimisticCount, setOptimisticCount] = useOptimistic(
count,
(_, newCount) => newCount
);
const increment = async () => {
setOptimisticCount(count + 1);
const result = await updateCount(count + 1);
setCount(result);
};
return (
<div>
<p>Count: {optimisticCount}</p>
<button onClick={increment}>+1</button>
</div>
);
}
// 时间线:
// 0ms: 用户点击
// 0ms: 显示新数字(立即)
// 500ms: 服务器确认
// 体验:0ms等待时间!
// ========== 方式3:手动乐观更新(旧方式)==========
function ManualOptimistic() {
const [count, setCount] = useState(0);
const [optimisticCount, setOptimisticCount] = useState(0);
const increment = async () => {
// 手动设置乐观状态
setOptimisticCount(count + 1);
try {
const result = await updateCount(count + 1);
setCount(result);
setOptimisticCount(result);
} catch (error) {
// 手动回滚
setOptimisticCount(count);
}
};
return (
<div>
<p>Count: {optimisticCount}</p>
<button onClick={increment}>+1</button>
</div>
);
}
// 问题:
// - 需要手动管理两个状态
// - 需要手动回滚
// - 容易出错
// - 代码冗长
// ========== 方式4:useOptimistic(推荐)==========
// 简洁、自动回滚、类型安全第二部分:基础用法
2.1 简单计数器
jsx
'use server';
export async function incrementCounter(currentValue) {
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1000));
// 更新数据库
await db.counters.update({
where: { id: 1 },
data: { value: currentValue + 1 }
});
return currentValue + 1;
}
// Client Component
'use client';
import { useOptimistic, useState } from 'react';
import { incrementCounter } from './actions';
export default function OptimisticCounter() {
const [count, setCount] = useState(0);
const [optimisticCount, setOptimisticCount] = useOptimistic(
count,
(currentCount, newCount) => newCount
);
const handleIncrement = async () => {
// 立即更新UI
setOptimisticCount(count + 1);
// 发送到服务器
const newCount = await incrementCounter(count);
// 更新实际状态
setCount(newCount);
};
return (
<div>
<p>计数: {optimisticCount}</p>
<button onClick={handleIncrement}>
增加
</button>
</div>
);
}
// 体验:
// - 点击按钮,计数立即增加(乐观)
// - 1秒后服务器响应确认
// - 如果失败,自动回滚2.2 点赞功能
jsx
// Server Action
'use server';
export async function likePost(postId) {
const session = await getSession();
if (!session) {
throw new Error('未登录');
}
await db.likes.create({
data: {
postId,
userId: session.userId
}
});
const likes = await db.likes.count({
where: { postId }
});
return likes;
}
// Client Component
'use client';
import { useOptimistic, useState } from 'react';
import { likePost } from './actions';
export default function LikeButton({ postId, initialLikes, initialLiked }) {
const [likes, setLikes] = useState(initialLikes);
const [liked, setLiked] = useState(initialLiked);
const [optimisticLikes, setOptimisticLikes] = useOptimistic(
likes,
(currentLikes, increment) => currentLikes + increment
);
const [optimisticLiked, setOptimisticLiked] = useOptimistic(
liked,
(_, newLiked) => newLiked
);
const handleLike = async () => {
if (liked) return; // 已经点赞
// 立即更新UI
setOptimisticLikes(1);
setOptimisticLiked(true);
try {
// 发送请求
const newLikes = await likePost(postId);
// 更新实际状态
setLikes(newLikes);
setLiked(true);
} catch (error) {
// 失败时自动回滚到原始状态
alert('点赞失败');
}
};
return (
<button
onClick={handleLike}
disabled={optimisticLiked}
className={optimisticLiked ? 'liked' : ''}
>
{optimisticLiked ? '❤️' : '🤍'} {optimisticLikes}
</button>
);
}2.3 切换状态
jsx
// Server Action
'use server';
export async function toggleTodo(todoId, currentCompleted) {
await db.todos.update({
where: { id: todoId },
data: { completed: !currentCompleted }
});
return !currentCompleted;
}
// Client Component
'use client';
import { useOptimistic } from 'react';
import { toggleTodo } from './actions';
export default function TodoItem({ todo }) {
const [optimisticCompleted, setOptimisticCompleted] = useOptimistic(
todo.completed,
(_, newCompleted) => newCompleted
);
const handleToggle = async () => {
// 立即切换状态
setOptimisticCompleted(!todo.completed);
try {
// 发送请求
await toggleTodo(todo.id, todo.completed);
} catch (error) {
// 失败会自动回滚
alert('更新失败');
}
};
return (
<div className={optimisticCompleted ? 'completed' : ''}>
<input
type="checkbox"
checked={optimisticCompleted}
onChange={handleToggle}
/>
<span>{todo.text}</span>
</div>
);
}2.4 输入字段实时更新
jsx
// Server Action
'use server';
export async function updateUserName(userId, name) {
await new Promise(resolve => setTimeout(resolve, 500));
await db.users.update({
where: { id: userId },
data: { name }
});
return name;
}
// Client Component
'use client';
import { useOptimistic, useState } from 'react';
import { updateUserName } from './actions';
export default function UserNameEditor({ userId, initialName }) {
const [name, setName] = useState(initialName);
const [optimisticName, setOptimisticName] = useOptimistic(
name,
(_, newName) => newName
);
const handleSave = async (newName) => {
// 立即更新显示
setOptimisticName(newName);
try {
await updateUserName(userId, newName);
setName(newName);
} catch (error) {
alert('保存失败');
}
};
return (
<div>
<p>当前名称:{optimisticName}</p>
<input
type="text"
defaultValue={name}
onBlur={(e) => handleSave(e.target.value)}
/>
</div>
);
}第三部分:高级用法
3.1 列表添加
jsx
// Server Action
'use server';
export async function addTodo(text) {
const todo = await db.todos.create({
data: {
text,
completed: false
}
});
return todo;
}
// Client Component
'use client';
import { useOptimistic, useState } from 'react';
import { addTodo } from './actions';
export default function TodoList({ initialTodos }) {
const [todos, setTodos] = useState(initialTodos);
const [optimisticTodos, setOptimisticTodos] = useOptimistic(
todos,
(currentTodos, newTodo) => [...currentTodos, newTodo]
);
const handleAdd = async (text) => {
// 创建临时ID的乐观Todo
const tempTodo = {
id: `temp-${Date.now()}`,
text,
completed: false,
pending: true // 标记为待确认
};
// 立即显示
setOptimisticTodos(tempTodo);
try {
// 发送请求
const newTodo = await addTodo(text);
// 更新实际状态(替换临时Todo)
setTodos(prev => [...prev, newTodo]);
} catch (error) {
alert('添加失败');
// 失败会自动从列表中移除
}
};
return (
<div>
<TodoForm onAdd={handleAdd} />
<ul>
{optimisticTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
isPending={todo.pending}
/>
))}
</ul>
</div>
);
}
function TodoItem({ todo, isPending }) {
return (
<li className={isPending ? 'pending' : ''}>
{todo.text}
{isPending && <span className="spinner">...</span>}
</li>
);
}3.2 列表删除
jsx
// Server Action
'use server';
export async function deleteTodo(todoId) {
await db.todos.delete({
where: { id: todoId }
});
return todoId;
}
// Client Component
'use client';
import { useOptimistic, useState } from 'react';
import { deleteTodo } from './actions';
export default function TodoList({ initialTodos }) {
const [todos, setTodos] = useState(initialTodos);
const [optimisticTodos, setOptimisticTodos] = useOptimistic(
todos,
(currentTodos, deletedId) =>
currentTodos.filter(todo => todo.id !== deletedId)
);
const handleDelete = async (todoId) => {
// 立即从列表中移除
setOptimisticTodos(todoId);
try {
// 发送删除请求
await deleteTodo(todoId);
// 更新实际状态
setTodos(prev => prev.filter(todo => todo.id !== todoId));
} catch (error) {
alert('删除失败');
// 失败会自动恢复到列表中
}
};
return (
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>
<span>{todo.text}</span>
<button onClick={() => handleDelete(todo.id)}>
删除
</button>
</li>
))}
</ul>
);
}3.3 复杂对象更新
jsx
// Server Action
'use server';
export async function updatePost(postId, updates) {
const post = await db.posts.update({
where: { id: postId },
data: updates
});
return post;
}
// Client Component
'use client';
import { useOptimistic, useState } from 'react';
import { updatePost } from './actions';
export default function Post({ initialPost }) {
const [post, setPost] = useState(initialPost);
const [optimisticPost, setOptimisticPost] = useOptimistic(
post,
(currentPost, updates) => ({
...currentPost,
...updates
})
);
const handleEdit = async (field, value) => {
// 立即更新UI
setOptimisticPost({ [field]: value });
try {
// 发送更新
const updatedPost = await updatePost(post.id, { [field]: value });
// 更新实际状态
setPost(updatedPost);
} catch (error) {
alert('更新失败');
}
};
return (
<article>
<h1
contentEditable
onBlur={(e) => handleEdit('title', e.target.textContent)}
>
{optimisticPost.title}
</h1>
<div
contentEditable
onBlur={(e) => handleEdit('content', e.target.textContent)}
>
{optimisticPost.content}
</div>
<span className="likes">
❤️ {optimisticPost.likes}
</span>
</article>
);
}3.4 列表排序
jsx
// Server Action
'use server';
export async function updateTodoOrder(todos) {
await db.$transaction(
todos.map((todo, index) =>
db.todos.update({
where: { id: todo.id },
data: { order: index }
})
)
);
return todos;
}
// Client Component
'use client';
import { useOptimistic, useState } from 'react';
import { updateTodoOrder } from './actions';
export default function SortableTodoList({ initialTodos }) {
const [todos, setTodos] = useState(initialTodos);
const [optimisticTodos, setOptimisticTodos] = useOptimistic(
todos,
(_, newTodos) => newTodos
);
const handleDragEnd = async (result) => {
if (!result.destination) return;
const newTodos = Array.from(todos);
const [removed] = newTodos.splice(result.source.index, 1);
newTodos.splice(result.destination.index, 0, removed);
// 立即更新UI
setOptimisticTodos(newTodos);
try {
// 保存新顺序
await updateTodoOrder(newTodos);
setTodos(newTodos);
} catch (error) {
alert('保存顺序失败');
}
};
return (
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="todos">
{(provided) => (
<ul {...provided.droppableProps} ref={provided.innerRef}>
{optimisticTodos.map((todo, index) => (
<Draggable key={todo.id} draggableId={todo.id} index={index}>
{(provided) => (
<li
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
{todo.text}
</li>
)}
</Draggable>
))}
{provided.placeholder}
</ul>
)}
</Droppable>
</DragDropContext>
);
}3.5 批量操作
jsx
// Server Action
'use server';
export async function bulkCompleteTodos(todoIds) {
await db.todos.updateMany({
where: { id: { in: todoIds } },
data: { completed: true }
});
return todoIds;
}
// Client Component
'use client';
import { useOptimistic, useState } from 'react';
import { bulkCompleteTodos } from './actions';
export default function TodoListWithBulk({ initialTodos }) {
const [todos, setTodos] = useState(initialTodos);
const [selectedIds, setSelectedIds] = useState([]);
const [optimisticTodos, setOptimisticTodos] = useOptimistic(
todos,
(currentTodos, completedIds) =>
currentTodos.map(todo =>
completedIds.includes(todo.id)
? { ...todo, completed: true }
: todo
)
);
const handleBulkComplete = async () => {
// 立即更新UI
setOptimisticTodos(selectedIds);
try {
// 批量更新
await bulkCompleteTodos(selectedIds);
// 更新实际状态
setTodos(prev =>
prev.map(todo =>
selectedIds.includes(todo.id)
? { ...todo, completed: true }
: todo
)
);
setSelectedIds([]);
} catch (error) {
alert('批量操作失败');
}
};
return (
<div>
<button
onClick={handleBulkComplete}
disabled={selectedIds.length === 0}
>
完成所选 ({selectedIds.length})
</button>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={selectedIds.includes(todo.id)}
onChange={(e) => {
setSelectedIds(prev =>
e.target.checked
? [...prev, todo.id]
: prev.filter(id => id !== todo.id)
);
}}
/>
<span className={todo.completed ? 'completed' : ''}>
{todo.text}
</span>
</li>
))}
</ul>
</div>
);
}3.6 嵌套列表操作
jsx
// Server Action
'use server';
export async function addComment(postId, text) {
const comment = await db.comments.create({
data: {
postId,
text,
createdAt: new Date()
}
});
return comment;
}
// Client Component
'use client';
import { useOptimistic, useState } from 'react';
import { addComment } from './actions';
export default function PostWithComments({ post, initialComments }) {
const [comments, setComments] = useState(initialComments);
const [optimisticComments, setOptimisticComments] = useOptimistic(
comments,
(currentComments, newComment) => [...currentComments, newComment]
);
const handleAddComment = async (text) => {
const tempComment = {
id: `temp-${Date.now()}`,
text,
createdAt: new Date(),
pending: true
};
// 立即显示评论
setOptimisticComments(tempComment);
try {
const comment = await addComment(post.id, text);
setComments(prev => [...prev, comment]);
} catch (error) {
alert('发表评论失败');
}
};
return (
<article>
<h2>{post.title}</h2>
<p>{post.content}</p>
<section className="comments">
<h3>评论 ({optimisticComments.length})</h3>
{optimisticComments.map(comment => (
<div
key={comment.id}
className={comment.pending ? 'pending' : ''}
>
<p>{comment.text}</p>
<time>{comment.createdAt.toLocaleString()}</time>
{comment.pending && <span>发送中...</span>}
</div>
))}
<CommentForm onSubmit={handleAddComment} />
</section>
</article>
);
}第四部分:错误处理
4.1 显示错误状态
jsx
'use client';
import { useOptimistic, useState } from 'react';
export default function LikeButtonWithError({ postId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const [error, setError] = useState(null);
const [optimisticLikes, setOptimisticLikes] = useOptimistic(
likes,
(current, increment) => current + increment
);
const handleLike = async () => {
setError(null);
// 乐观更新
setOptimisticLikes(1);
try {
const newLikes = await likePost(postId);
setLikes(newLikes);
} catch (error) {
setError('点赞失败,请重试');
// useOptimistic会自动回滚
}
};
return (
<div>
<button onClick={handleLike}>
❤️ {optimisticLikes}
</button>
{error && (
<div className="error">
{error}
</div>
)}
</div>
);
}4.2 重试机制
jsx
'use client';
import { useOptimistic, useState } from 'react';
export default function TodoWithRetry({ todo }) {
const [completed, setCompleted] = useState(todo.completed);
const [retrying, setRetrying] = useState(false);
const [optimisticCompleted, setOptimisticCompleted] = useOptimistic(
completed,
(_, newValue) => newValue
);
const toggleWithRetry = async (retryCount = 0) => {
setRetrying(retryCount > 0);
// 乐观更新
setOptimisticCompleted(!completed);
try {
await toggleTodo(todo.id);
setCompleted(!completed);
setRetrying(false);
} catch (error) {
if (retryCount < 3) {
// 自动重试
setTimeout(() => {
toggleWithRetry(retryCount + 1);
}, 1000 * (retryCount + 1));
} else {
// 重试失败
alert('操作失败,请稍后重试');
setRetrying(false);
}
}
};
return (
<div>
<input
type="checkbox"
checked={optimisticCompleted}
onChange={() => toggleWithRetry()}
/>
<span>{todo.text}</span>
{retrying && <span className="retrying">重试中...</span>}
</div>
);
}4.3 显示待确认状态
jsx
'use client';
import { useOptimistic, useState } from 'react';
export default function TodoWithPendingState({ todo }) {
const [completed, setCompleted] = useState(todo.completed);
const [isPending, setIsPending] = useState(false);
const [optimisticCompleted, setOptimisticCompleted] = useOptimistic(
completed,
(_, newValue) => newValue
);
const handleToggle = async () => {
setIsPending(true);
// 乐观更新
setOptimisticCompleted(!completed);
try {
await toggleTodo(todo.id);
setCompleted(!completed);
} catch (error) {
alert('更新失败');
} finally {
setIsPending(false);
}
};
return (
<div className={isPending ? 'pending' : ''}>
<input
type="checkbox"
checked={optimisticCompleted}
onChange={handleToggle}
/>
<span>{todo.text}</span>
{isPending && <Spinner />}
</div>
);
}
/* CSS */
.pending {
opacity: 0.7;
pointer-events: none;
}4.4 错误边界处理
jsx
'use client';
import { useOptimistic, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
function FallbackComponent({ error, resetErrorBoundary }) {
return (
<div className="error-container">
<h3>操作失败</h3>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>重试</button>
</div>
);
}
export default function TodoListWithErrorBoundary({ initialTodos }) {
const [todos, setTodos] = useState(initialTodos);
const [optimisticTodos, setOptimisticTodos] = useOptimistic(
todos,
(currentTodos, action) => {
switch (action.type) {
case 'add':
return [...currentTodos, action.todo];
case 'delete':
return currentTodos.filter(t => t.id !== action.id);
case 'update':
return currentTodos.map(t =>
t.id === action.id ? { ...t, ...action.updates } : t
);
default:
return currentTodos;
}
}
);
const handleAdd = async (text) => {
const tempTodo = {
id: `temp-${Date.now()}`,
text,
pending: true
};
setOptimisticTodos({ type: 'add', todo: tempTodo });
try {
const newTodo = await addTodo(text);
setTodos(prev => [...prev, newTodo]);
} catch (error) {
throw new Error('添加失败:' + error.message);
}
};
return (
<ErrorBoundary FallbackComponent={FallbackComponent}>
<div>
<TodoForm onAdd={handleAdd} />
<TodoItems todos={optimisticTodos} />
</div>
</ErrorBoundary>
);
}4.5 Toast通知
jsx
'use client';
import { useOptimistic, useState } from 'react';
import { toast } from 'react-hot-toast';
export default function TodoWithToast({ initialTodos }) {
const [todos, setTodos] = useState(initialTodos);
const [optimisticTodos, setOptimisticTodos] = useOptimistic(
todos,
(currentTodos, action) => {
if (action.type === 'delete') {
return currentTodos.filter(t => t.id !== action.id);
}
return currentTodos;
}
);
const handleDelete = async (todoId) => {
// 立即更新UI
setOptimisticTodos({ type: 'delete', id: todoId });
// 显示加载提示
const toastId = toast.loading('删除中...');
try {
await deleteTodo(todoId);
setTodos(prev => prev.filter(t => t.id !== todoId));
// 成功提示
toast.success('删除成功', { id: toastId });
} catch (error) {
// 失败提示
toast.error('删除失败:' + error.message, { id: toastId });
}
};
return (
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>
<span>{todo.text}</span>
<button onClick={() => handleDelete(todo.id)}>删除</button>
</li>
))}
</ul>
);
}第五部分:性能优化
5.1 防抖处理
jsx
'use client';
import { useOptimistic, useState, useCallback } from 'react';
import { debounce } from 'lodash';
export default function SearchWithOptimistic() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [optimisticQuery, setOptimisticQuery] = useOptimistic(
query,
(_, newQuery) => newQuery
);
// 防抖搜索
const debouncedSearch = useCallback(
debounce(async (searchQuery) => {
try {
const results = await searchPosts(searchQuery);
setQuery(searchQuery);
setResults(results);
} catch (error) {
toast.error('搜索失败');
}
}, 500),
[]
);
const handleSearch = (value) => {
// 立即更新搜索框
setOptimisticQuery(value);
// 防抖发送请求
debouncedSearch(value);
};
return (
<div>
<input
type="search"
value={optimisticQuery}
onChange={(e) => handleSearch(e.target.value)}
placeholder="搜索..."
/>
<SearchResults results={results} query={optimisticQuery} />
</div>
);
}5.2 节流处理
jsx
'use client';
import { useOptimistic, useState, useRef } from 'react';
export default function LikeButtonThrottled({ postId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const isThrottled = useRef(false);
const [optimisticLikes, setOptimisticLikes] = useOptimistic(
likes,
(current, increment) => current + increment
);
const handleLike = async () => {
// 节流:1秒内只能点击一次
if (isThrottled.current) return;
isThrottled.current = true;
setTimeout(() => {
isThrottled.current = false;
}, 1000);
// 乐观更新
setOptimisticLikes(1);
try {
const newLikes = await likePost(postId);
setLikes(newLikes);
} catch (error) {
toast.error('点赞失败');
}
};
return (
<button onClick={handleLike}>
❤️ {optimisticLikes}
</button>
);
}5.3 批量更新优化
jsx
'use client';
import { useOptimistic, useState, useRef } from 'react';
export default function BatchUpdateTodos({ initialTodos }) {
const [todos, setTodos] = useState(initialTodos);
const updateQueue = useRef([]);
const flushTimer = useRef(null);
const [optimisticTodos, setOptimisticTodos] = useOptimistic(
todos,
(currentTodos, action) => {
return currentTodos.map(todo =>
todo.id === action.id
? { ...todo, ...action.updates }
: todo
);
}
);
const flushUpdates = async () => {
if (updateQueue.current.length === 0) return;
const updates = [...updateQueue.current];
updateQueue.current = [];
try {
await batchUpdateTodos(updates);
setTodos(prev =>
prev.map(todo => {
const update = updates.find(u => u.id === todo.id);
return update ? { ...todo, ...update.updates } : todo;
})
);
} catch (error) {
toast.error('批量更新失败');
}
};
const handleUpdate = (id, updates) => {
// 立即更新UI
setOptimisticTodos({ id, updates });
// 添加到更新队列
updateQueue.current.push({ id, updates });
// 延迟批量提交
clearTimeout(flushTimer.current);
flushTimer.current = setTimeout(flushUpdates, 1000);
};
return (
<ul>
{optimisticTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} onUpdate={handleUpdate} />
))}
</ul>
);
}第六部分:实战案例
6.1 社交媒体点赞系统
jsx
// Server Actions
'use server';
export async function toggleLike(postId, userId, currentLiked) {
if (currentLiked) {
// 取消点赞
await db.likes.delete({
where: {
postId_userId: { postId, userId }
}
});
} else {
// 点赞
await db.likes.create({
data: { postId, userId }
});
}
const likesCount = await db.likes.count({
where: { postId }
});
return { liked: !currentLiked, likesCount };
}
// Client Component
'use client';
import { useOptimistic, useState } from 'react';
import { toggleLike } from './actions';
export default function SocialPost({ post, userId, initialLiked, initialLikesCount }) {
const [liked, setLiked] = useState(initialLiked);
const [likesCount, setLikesCount] = useState(initialLikesCount);
const [optimisticLiked, setOptimisticLiked] = useOptimistic(
liked,
(_, newLiked) => newLiked
);
const [optimisticLikesCount, setOptimisticLikesCount] = useOptimistic(
likesCount,
(currentCount, increment) => currentCount + increment
);
const handleToggleLike = async () => {
// 立即更新UI
setOptimisticLiked(!liked);
setOptimisticLikesCount(liked ? -1 : 1);
try {
const result = await toggleLike(post.id, userId, liked);
setLiked(result.liked);
setLikesCount(result.likesCount);
} catch (error) {
toast.error('操作失败');
}
};
return (
<article className="social-post">
<header>
<img src={post.author.avatar} alt={post.author.name} />
<span>{post.author.name}</span>
</header>
<div className="content">{post.content}</div>
<footer>
<button
onClick={handleToggleLike}
className={optimisticLiked ? 'liked' : ''}
>
{optimisticLiked ? '❤️' : '🤍'}
<span>{optimisticLikesCount}</span>
</button>
</footer>
</article>
);
}6.2 实时协作编辑器
jsx
// Server Action
'use server';
export async function updateDocument(docId, content) {
const doc = await db.documents.update({
where: { id: docId },
data: {
content,
updatedAt: new Date()
}
});
return doc;
}
// Client Component
'use client';
import { useOptimistic, useState, useEffect } from 'react';
import { updateDocument } from './actions';
export default function CollaborativeEditor({ initialDoc }) {
const [doc, setDoc] = useState(initialDoc);
const [optimisticDoc, setOptimisticDoc] = useOptimistic(
doc,
(currentDoc, updates) => ({ ...currentDoc, ...updates })
);
const handleContentChange = async (newContent) => {
// 立即更新UI
setOptimisticDoc({ content: newContent });
try {
const updatedDoc = await updateDocument(doc.id, newContent);
setDoc(updatedDoc);
} catch (error) {
toast.error('保存失败');
}
};
return (
<div className="editor">
<header>
<h2>{optimisticDoc.title}</h2>
<span className="last-updated">
最后更新:{optimisticDoc.updatedAt.toLocaleString()}
</span>
</header>
<textarea
value={optimisticDoc.content}
onChange={(e) => handleContentChange(e.target.value)}
placeholder="开始编辑..."
/>
</div>
);
}6.3 购物车系统
jsx
// Server Actions
'use server';
export async function addToCart(productId, quantity) {
const cartItem = await db.cartItems.create({
data: {
productId,
quantity,
userId: (await getSession()).userId
},
include: {
product: true
}
});
return cartItem;
}
export async function updateCartQuantity(itemId, quantity) {
const cartItem = await db.cartItems.update({
where: { id: itemId },
data: { quantity },
include: {
product: true
}
});
return cartItem;
}
export async function removeFromCart(itemId) {
await db.cartItems.delete({
where: { id: itemId }
});
return itemId;
}
// Client Component
'use client';
import { useOptimistic, useState } from 'react';
import { addToCart, updateCartQuantity, removeFromCart } from './actions';
export default function ShoppingCart({ initialCartItems }) {
const [cartItems, setCartItems] = useState(initialCartItems);
const [optimisticCartItems, setOptimisticCartItems] = useOptimistic(
cartItems,
(currentItems, action) => {
switch (action.type) {
case 'add':
return [...currentItems, action.item];
case 'update':
return currentItems.map(item =>
item.id === action.id
? { ...item, quantity: action.quantity }
: item
);
case 'remove':
return currentItems.filter(item => item.id !== action.id);
default:
return currentItems;
}
}
);
const handleAddToCart = async (product) => {
const tempItem = {
id: `temp-${Date.now()}`,
product,
quantity: 1,
pending: true
};
setOptimisticCartItems({ type: 'add', item: tempItem });
try {
const newItem = await addToCart(product.id, 1);
setCartItems(prev => [...prev, newItem]);
toast.success('已添加到购物车');
} catch (error) {
toast.error('添加失败');
}
};
const handleUpdateQuantity = async (itemId, newQuantity) => {
setOptimisticCartItems({ type: 'update', id: itemId, quantity: newQuantity });
try {
const updatedItem = await updateCartQuantity(itemId, newQuantity);
setCartItems(prev =>
prev.map(item => (item.id === itemId ? updatedItem : item))
);
} catch (error) {
toast.error('更新失败');
}
};
const handleRemove = async (itemId) => {
setOptimisticCartItems({ type: 'remove', id: itemId });
try {
await removeFromCart(itemId);
setCartItems(prev => prev.filter(item => item.id !== itemId));
toast.success('已从购物车移除');
} catch (error) {
toast.error('移除失败');
}
};
const total = optimisticCartItems.reduce(
(sum, item) => sum + item.product.price * item.quantity,
0
);
return (
<div className="shopping-cart">
<h2>购物车 ({optimisticCartItems.length})</h2>
{optimisticCartItems.map(item => (
<div key={item.id} className={item.pending ? 'pending' : ''}>
<img src={item.product.image} alt={item.product.name} />
<div>
<h3>{item.product.name}</h3>
<p>¥{item.product.price}</p>
</div>
<div className="quantity">
<button onClick={() => handleUpdateQuantity(item.id, item.quantity - 1)}>
-
</button>
<span>{item.quantity}</span>
<button onClick={() => handleUpdateQuantity(item.id, item.quantity + 1)}>
+
</button>
</div>
<button onClick={() => handleRemove(item.id)}>删除</button>
</div>
))}
<footer>
<div className="total">总计:¥{total.toFixed(2)}</div>
<button className="checkout">结算</button>
</footer>
</div>
);
}注意事项
1. useOptimistic必须在Client Component中
jsx
// ❌ 错误:在Server Component中
async function ServerComponent() {
const [optimistic, setOptimistic] = useOptimistic(...); // 错误!
}
// ✅ 正确:在Client Component中
'use client';
function ClientComponent() {
const [optimistic, setOptimistic] = useOptimistic(...); // 正确
}2. 失败时自动回滚
jsx
// useOptimistic会自动回滚
const handleLike = async () => {
setOptimisticLikes(likes + 1); // 乐观更新
try {
await likePost(postId);
} catch (error) {
// 无需手动回滚
// useOptimistic会自动恢复到原始状态
}
};3. 更新实际状态
jsx
// ✅ 成功后更新实际状态
const handleLike = async () => {
setOptimisticLikes(likes + 1);
const newLikes = await likePost(postId);
// 必须更新实际状态
setLikes(newLikes);
};4. 避免过度使用
jsx
// ❌ 不适合乐观更新
// - 复杂的业务逻辑
// - 低成功率操作
// - 需要服务器验证的操作
// ✅ 适合乐观更新
// - 简单的CRUD操作
// - 高成功率操作
// - 用户频繁操作5. 处理竞态条件
jsx
'use client';
import { useOptimistic, useState, useRef } from 'react';
export default function SearchWithRaceCondition() {
const [query, setQuery] = useState('');
const requestId = useRef(0);
const [optimisticQuery, setOptimisticQuery] = useOptimistic(
query,
(_, newQuery) => newQuery
);
const handleSearch = async (searchQuery) => {
// 生成请求ID
const currentRequestId = ++requestId.current;
// 乐观更新
setOptimisticQuery(searchQuery);
try {
const results = await searchPosts(searchQuery);
// 只处理最新的请求
if (currentRequestId === requestId.current) {
setQuery(searchQuery);
}
} catch (error) {
toast.error('搜索失败');
}
};
return (
<input
type="search"
value={optimisticQuery}
onChange={(e) => handleSearch(e.target.value)}
/>
);
}6. TypeScript类型定义
typescript
import { useOptimistic } from 'react';
interface Todo {
id: string;
text: string;
completed: boolean;
}
type TodoAction =
| { type: 'add'; todo: Todo }
| { type: 'delete'; id: string }
| { type: 'update'; id: string; updates: Partial<Todo> };
function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
const [todos, setTodos] = useState<Todo[]>(initialTodos);
const [optimisticTodos, setOptimisticTodos] = useOptimistic<Todo[], TodoAction>(
todos,
(currentTodos, action) => {
switch (action.type) {
case 'add':
return [...currentTodos, action.todo];
case 'delete':
return currentTodos.filter(t => t.id !== action.id);
case 'update':
return currentTodos.map(t =>
t.id === action.id ? { ...t, ...action.updates } : t
);
default:
return currentTodos;
}
}
);
// ...
}常见问题
Q1: 什么时候使用乐观更新?
A: 适合以下场景:
- 用户操作频繁(点赞、切换状态)
- 成功率高(网络良好)
- 延迟明显(需要提升体验)
- 需要即时反馈(提高流畅度)
Q2: 乐观更新失败了怎么办?
A: useOptimistic会自动回滚到原始状态,你只需提供错误提示即可。无需手动处理回滚逻辑。
Q3: 可以同时有多个乐观状态吗?
A: 可以!每个useOptimistic独立管理自己的状态,互不干扰。
jsx
const [optimisticLikes, setOptimisticLikes] = useOptimistic(likes, ...);
const [optimisticComments, setOptimisticComments] = useOptimistic(comments, ...);Q4: useOptimistic和useState有什么区别?
A:
- useState:通用状态管理,需要手动处理所有逻辑
- useOptimistic:专门用于乐观更新,自动处理回滚
Q5: 如何处理并发更新?
A: 使用版本号或时间戳:
jsx
const [data, setData] = useState({ value: 0, version: 0 });
const [optimisticData, setOptimisticData] = useOptimistic(
data,
(current, update) => ({
...current,
...update,
version: current.version + 1
})
);Q6: 乐观更新会影响SEO吗?
A: 不会。乐观更新只影响客户端交互,不影响服务端渲染和SEO。
Q7: 如何测试乐观更新?
A: 使用网络节流模拟慢速网络:
javascript
// 在Chrome DevTools中
// Network -> Throttling -> Slow 3G
// 或在Server Action中添加延迟
await new Promise(resolve => setTimeout(resolve, 2000));总结
乐观更新的核心价值
✅ 即时UI反馈
✅ 流畅用户体验
✅ 隐藏网络延迟
✅ 自动错误回滚
✅ 简化代码逻辑
✅ 提升应用感知性能
✅ 减少用户等待时间适用场景
✅ 点赞/收藏
✅ 待办事项切换
✅ 列表增删改
✅ 简单状态切换
✅ 高成功率操作
✅ 社交互动
✅ 实时协作不适用场景
❌ 支付操作
❌ 关键业务逻辑
❌ 需要强一致性的操作
❌ 低成功率操作
❌ 复杂验证逻辑最佳实践
1. 只在Client Component中使用
2. 成功后更新实际状态
3. 提供错误提示
4. 显示待确认状态
5. 考虑重试机制
6. 保持操作幂等性
7. 处理竞态条件
8. 合理使用防抖节流
9. 添加TypeScript类型
10. 充分测试错误场景性能考虑
✅ 减少不必要的网络请求
✅ 使用防抖处理高频操作
✅ 批量处理多个更新
✅ 避免过度渲染
✅ 合理设置重试策略与其他特性结合
jsx
// 1. 与useActionState结合
const [state, formAction] = useActionState(serverAction, initialState);
// 2. 与useFormStatus结合
const { pending } = useFormStatus();
// 3. 与useTransition结合
const [isPending, startTransition] = useTransition();
// 4. 与Server Actions结合
'use server';
export async function updateData(formData) {
// ...
}useOptimistic让React应用响应更快、体验更好!