React 19 新特性:`useOptimistic` Hook 完整指南

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>
  );
}

核心注意点

  1. 乐观状态与真实状态分离

    • optimisticValue 是暂时的 UI 状态
    • setState 才是最终真实状态
  2. 失败回退需要手动处理

    • React 不知道服务器请求是否失败
    • 必须在 catch 中使用 reducer 回退乐观值
  3. 支持多次并发更新

    • useOptimistic 内部会管理队列,多个乐观更新不会互相覆盖
  4. 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,是前端异步交互的利器。

相关推荐
弓.长.2 小时前
ReactNative for OpenHarmony项目鸿蒙化三方库:react-native-image-crop-picker — 图片选择裁剪组件
react native·react.js·harmonyos
清汤饺子2 小时前
$20 的 Cursor Pro 额度,这样用一个月都花不完
前端·javascript·后端
a1117762 小时前
MD 架构图生成器(html 开源)
前端·开源·html
肠胃炎2 小时前
树形选择器组件封装
前端·flutter
CHU7290352 小时前
一番赏爬塔闯关小程序前端功能玩法设计解析
前端·小程序
ℋᙚᵐⁱᒻᵉ鲸落2 小时前
Vue3 分页加载避坑指南:如何解决“向下滚动时出现重复数据”的问题?
前端·vue.js
smchaopiao2 小时前
理解HTML中的段落标签:功能与应用
前端·css·html
云原生指北2 小时前
AI Agent 的代码执行沙箱:从容器到微虚拟机的隔离之道
前端
Fairy要carry3 小时前
面试-Agent Loop
前端·chrome