React 19 新特性:useOptimistic Hook 完整指南
在 React 19 中,官方引入了一个全新的 Hook ------ useOptimistic ,用于处理 乐观 UI 更新(Optimistic UI) 。
它能够让你的应用在用户操作时立即更新 UI,而无需等待异步操作完成,从而提供更流畅的交互体验。
本文将深入讲解 useOptimistic 的用法,并提供示例代码。
什么是乐观更新?
乐观更新是一种常见的 UI 技术:
- 用户触发操作(例如点赞、收藏、发送消息)
- UI 立即反应,不等待服务器响应
- 异步请求成功时,保持 UI 状态
- 异步请求失败时,回退或提示错误
传统实现方式通常需要手动管理临时状态和回退逻辑,而 useOptimistic 帮助我们更优雅地管理这一流程。
useOptimistic 基本语法
js
const [optimisticValue, updateOptimistic] = useOptimistic(initialValue, reducer);
initialValue:初始状态值reducer(current, action):一个 reducer 函数,用于根据 action 更新乐观状态optimisticValue:当前乐观状态updateOptimistic(action):触发乐观状态更新
示例:简单计数器
jsx
import { useOptimistic } from 'react';
function Counter() {
const [count, updateCount] = useOptimistic(0, (current, action) => current + action);
return (
<div>
<p>计数: {count}</p>
<button onClick={() => updateCount(1)}>+1</button>
</div>
);
}
点击按钮时,UI 会立即增加数字,而不依赖任何异步请求。
与异步请求结合
乐观更新的最大价值在于异步操作 。
例如,我们模拟一个"点赞按钮",并假设请求有 50% 概率失败:
jsx
import { useState, useTransition, useOptimistic } from 'react';
export default function LikeButton() {
const [likes, setLikes] = useState(0);
const [optimisticLikes, addOptimisticLikes] = useOptimistic(likes, (current, action) => current + action);
const [isPending, startTransition] = useTransition();
const [error, setError] = useState('');
const handleLike = () => {
setError('');
addOptimisticLikes(1); // 乐观更新
startTransition(async () => {
try {
await new Promise(r => setTimeout(r, 1000)); // 模拟延迟
const success = Math.random() < 0.5; // 50% 概率失败
if (!success) throw new Error('服务器请求失败');
setLikes(prev => prev + 1); // 成功更新真实状态
} catch (err) {
setError(err.message);
addOptimisticLikes(-1); // 回退乐观值
}
});
};
return (
<div style={{ fontFamily: 'sans-serif', padding: 20 }}>
<p>点赞数: {optimisticLikes}</p>
<button onClick={handleLike} disabled={isPending}>
{isPending ? '处理中...' : '点赞'}
</button>
{error && <p style={{ color: 'red' }}>⚠️ {error}</p>}
</div>
);
}
核心注意点
-
乐观状态与真实状态分离
optimisticValue是暂时的 UI 状态setState才是最终真实状态
-
失败回退需要手动处理
- React 不知道服务器请求是否失败
- 必须在 catch 中使用 reducer 回退乐观值
-
支持多次并发更新
useOptimistic内部会管理队列,多个乐观更新不会互相覆盖
-
与
useTransition搭配使用- 可以控制异步更新的优先级
- 提供加载状态(
isPending)优化 UX
完整Demo
tsx
import React, {
useState,
useOptimistic,
useCallback,
useTransition,
} from "react";
// 类型定义
interface LikeButtonProps {
id: string;
initialLikes: number;
}
interface ApiResponse {
likes: number;
success: boolean;
}
function LikeButton({ id, initialLikes }: LikeButtonProps) {
// 实际状态
const [likes, setLikes] = useState<number>(initialLikes);
// 乐观更新状态
const [optimisticLikes, addOptimisticLike] = useOptimistic(
likes,
(currentLikes: number, action: { type: "like" | "unlike" | "reset" }) => {
switch (action.type) {
case "like":
return currentLikes + 1;
case "unlike":
return Math.max(0, currentLikes - 1);
case "reset":
return initialLikes;
default:
return currentLikes;
}
},
);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
// ✅ 必须使用 useTransition
const [isPending, startTransition] = useTransition();
// 模拟 API 调用
const simulateApiCall = async (
action: "like" | "unlike" | "reset",
currentLikes: number,
): Promise<ApiResponse> => {
console.log("Starting API call:", action);
await new Promise((resolve) => setTimeout(resolve, 2000));
if (Math.random() < 0.2) {
console.log("API call failed");
throw new Error("网络请求失败,请重试");
}
switch (action) {
case "like":
return { likes: currentLikes + 1, success: true };
case "unlike":
return { likes: Math.max(0, currentLikes - 1), success: true };
case "reset":
return { likes: initialLikes, success: true };
}
};
// ============================================
// ✅ 修复:用 startTransition 包裹乐观更新
// ============================================
const handleLike = useCallback(async () => {
console.log("=== Handle Like clicked ===");
setError(null);
setIsLoading(true);
const previousLikes = likes;
// 关键:把乐观更新放在 startTransition 中
startTransition(() => {
console.log("1. Optimistic update inside transition");
addOptimisticLike({ type: "like" });
});
startTransition(async () => {
try {
console.log("2. Starting API call...");
const result = await simulateApiCall("like", previousLikes);
console.log("3. API success, updating real state");
setLikes(result.likes);
} catch (err) {
console.log("4. API failed, rolling back");
// 回滚:同样要在 transition 中
setLikes(previousLikes);
setError(err instanceof Error ? err.message : "未知错误");
} finally {
setIsLoading(false);
}
});
}, [likes]);
const handleUnlike = useCallback(async () => {
setError(null);
setIsLoading(true);
const previousLikes = likes;
startTransition(() => {
addOptimisticLike({ type: "unlike" });
});
startTransition(async () => {
try {
const result = await simulateApiCall("unlike", previousLikes);
setLikes(result.likes);
} catch (err) {
setLikes(previousLikes);
setError(err instanceof Error ? err.message : "未知错误");
} finally {
setIsLoading(false);
}
});
}, [likes]);
const handleReset = useCallback(async () => {
setError(null);
setIsLoading(true);
const previousLikes = likes;
startTransition(() => {
addOptimisticLike({ type: "reset" });
});
try {
const result = await simulateApiCall("reset", previousLikes);
setLikes(result.likes);
} catch (err) {
startTransition(() => {
setLikes(previousLikes);
});
setError(err instanceof Error ? err.message : "未知错误");
} finally {
setIsLoading(false);
}
}, [likes, initialLikes]);
const clearError = () => setError(null);
return (
<div
style={{
padding: "30px",
border: "2px solid #e0e0e0",
borderRadius: "12px",
maxWidth: "600px",
margin: "0 auto",
fontFamily: "system-ui, -apple-system, sans-serif",
}}
>
<h2 style={{ textAlign: "center", color: "#333", marginBottom: "30px" }}>
useOptimistic 完整示例(React 19.2 修复版)
</h2>
{/* 状态对比区域 */}
<div
style={{
display: "flex",
justifyContent: "space-around",
margin: "30px 0",
padding: "20px",
backgroundColor: "#f8f9fa",
borderRadius: "8px",
}}
>
<div style={{ textAlign: "center" }}>
<h3
style={{ color: "#2196f3", marginBottom: "10px", fontSize: "14px" }}
>
实际状态 (Real State)
</h3>
<div style={{ fontSize: "48px", fontWeight: "bold", color: "#333" }}>
{likes}
</div>
<p style={{ color: "#666", marginTop: "5px", fontSize: "12px" }}>
来自服务器的真实数据
</p>
</div>
<div style={{ textAlign: "center" }}>
<h3
style={{ color: "#ff9800", marginBottom: "10px", fontSize: "14px" }}
>
乐观状态 (Optimistic State)
</h3>
<div
style={{
fontSize: "48px",
fontWeight: "bold",
color: optimisticLikes !== likes ? "#ff9800" : "#4caf50",
transition: "color 0.3s",
}}
>
{optimisticLikes}
</div>
<p style={{ color: "#666", marginTop: "5px", fontSize: "12px" }}>
立即更新的 UI 状态
</p>
</div>
</div>
{/* 状态指示器 */}
{optimisticLikes !== likes && !error && (
<div
style={{
padding: "15px",
backgroundColor: "#fff3e0",
borderRadius: "8px",
marginBottom: "20px",
textAlign: "center",
}}
>
<span style={{ color: "#ff9800", fontWeight: "bold" }}>
⚡ 乐观更新中... 等待服务器响应
</span>
</div>
)}
{/* 操作按钮区域 */}
<div
style={{
display: "flex",
justifyContent: "center",
gap: "15px",
margin: "30px 0",
}}
>
<button
onClick={handleLike}
disabled={isLoading}
style={{
padding: "15px 30px",
fontSize: "16px",
fontWeight: "bold",
cursor: isLoading ? "not-allowed" : "pointer",
backgroundColor: isLoading ? "#ccc" : "#2196f3",
color: "white",
border: "none",
borderRadius: "8px",
}}
>
{isLoading ? "⏳ 处理中..." : `👍 点赞`}
</button>
<button
onClick={handleUnlike}
disabled={isLoading || likes === 0}
style={{
padding: "15px 30px",
fontSize: "16px",
fontWeight: "bold",
cursor: isLoading || likes === 0 ? "not-allowed" : "pointer",
backgroundColor: isLoading || likes === 0 ? "#ccc" : "#f44336",
color: "white",
border: "none",
borderRadius: "8px",
}}
>
{isLoading ? "⏳ 处理中..." : "👎 取消点赞"}
</button>
<button
onClick={handleReset}
disabled={isLoading}
style={{
padding: "15px 30px",
fontSize: "16px",
fontWeight: "bold",
cursor: isLoading ? "not-allowed" : "pointer",
backgroundColor: isLoading ? "#ccc" : "#9c27b0",
color: "white",
border: "none",
borderRadius: "8px",
}}
>
{isLoading ? "⏳ 处理中..." : "🔄 重置"}
</button>
</div>
{/* 错误提示 */}
{error && (
<div
style={{
padding: "15px",
backgroundColor: "#ffebee",
borderRadius: "8px",
marginBottom: "20px",
textAlign: "center",
}}
>
<span style={{ color: "#c62828", fontWeight: "bold" }}>
❌ {error}
</span>
</div>
)}
{/* 说明 */}
<div
style={{
marginTop: "20px",
padding: "15px",
backgroundColor: "#e3f2fd",
borderRadius: "8px",
}}
>
<p style={{ margin: 0, color: "#333", fontSize: "14px" }}>
<strong>💡 React 19.2 更新:</strong>useOptimistic 必须在
useTransition 中使用!
</p>
</div>
</div>
);
}
export default LikeButton;
总结
useOptimistic 是 React 19 提供的 乐观 UI 管理工具,它可以:
- 立即更新 UI
- 合并多个乐观更新
- 与异步操作无缝配合
适合以下场景:
- 点赞 / 收藏 / 评论
- 表单提交预览
- 实时数据交互(如聊天、投票)
乐观更新不仅提高了响应速度,还让应用体验更加流畅。
🎯 小贴士
- 异步操作失败时,务必回退乐观值
- 多次乐观更新时,确保 reducer 是幂等的
- 可以结合
useTransition显示加载状态或禁用按钮
React 19 的 useOptimistic 提供了一种优雅的方式管理乐观 UI,是前端异步交互的利器。