React从入门到出门第八章 React19新特性use()/useOptimistic 原理与业务落地

大家好~ React 19 里有不少令人兴奋的新特性,其中 use()useOptimistic() 两个新 Hooks 堪称"业务效率提升利器"。前者彻底改变了异步数据在组件中的使用方式,后者让乐观更新的实现成本直降为零。

但很多开发者在初次接触这两个 Hooks 时,难免会有困惑:use() 和 useEffect、Suspense 是什么关系?useOptimistic 到底解决了什么痛点?实际业务中该怎么落地?今天这篇文章,我们就从"痛点分析"出发,一步步拆解这两个 Hooks 的核心原理,再通过 5 个真实业务案例,带你彻底掌握它们的使用方法和落地技巧,让新特性真正服务于业务~

一、先搞懂:React 19 为什么要新增这两个 Hooks?

在聊具体用法之前,我们先回到业务场景,看看过去的 React 版本中,我们面临哪些棘手的问题------这正是新 Hooks 诞生的原因。

1. 痛点 1:异步数据获取的"模板代码冗余"

在 React 18 及之前,我们获取异步数据(比如接口请求),通常需要用 useState 管理加载状态、数据状态、错误状态,再用 useEffect 触发请求,代码模板化严重:

javascript 复制代码
// React 18 及之前的异步数据获取
function UserProfile({ userId }) {
  // 3 个状态管理:加载中、数据、错误
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 重置状态
    setLoading(true);
    setError(null);

    // 接口请求
    const fetchUser = async () => {
      try {
        const res = await fetch(`/api/users/${userId}`);
        if (!res.ok) throw new Error('请求失败');
        const data = await res.json();
        setUser(data); // 成功设置数据
      } catch (err) {
        setError(err.message); // 失败设置错误
      } finally {
        setLoading(false); // 无论成败,结束加载
      }
    };

    fetchUser();
  }, [userId]);

  // 加载中、错误、成功的条件渲染
  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误:{error}</div>;
  if (!user) return <div>无数据</div>;

  return (
    <div>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
}

这种写法的问题很明显:每个异步请求都要重复写"状态管理+错误处理+条件渲染"的模板代码,代码冗余,且容易遗漏边缘情况(比如组件卸载前请求还没完成导致的内存泄漏)。

2. 痛点 2:乐观更新的"实现复杂且易出错"

乐观更新是提升用户体验的关键技巧------比如用户点击"点赞"按钮时,先立即更新页面状态,再发送请求,等请求成功后确认状态,失败后回滚。但在 React 18 及之前,实现乐观更新需要手动管理"临时状态"和"真实状态",逻辑繁琐且容易出错:

javascript 复制代码
// React 18 及之前的乐观更新实现(点赞功能)
function LikeButton({ postId, initialLikes }) {
  const [likes, setLikes] = useState(initialLikes);
  const [isPending, setIsPending] = useState(false); // 标记请求中

  const handleLike = async () => {
    // 1. 乐观更新:先修改本地状态
    const prevLikes = likes;
    setLikes(prev => prev + 1);
    setIsPending(true);

    try {
      // 2. 发送请求
      const res = await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
      if (!res.ok) throw new Error('点赞失败');
    } catch (err) {
      // 3. 请求失败:回滚状态
      setLikes(prevLikes);
      alert(err.message);
    } finally {
      // 4. 结束请求状态
      setIsPending(false);
    }
  };

  return (
    <button onClick={handleLike} disabled={isPending}>
      点赞 ({likes})
    </button>
  );
}

这种写法需要手动记录"之前的状态"用于回滚,还要处理"请求中状态"防止重复点击,逻辑分散在多个地方,维护成本高。

3. 新 Hooks 的核心价值

React 19 新增的 use()useOptimistic(),正是为了解决这两个痛点:

  • use():简化异步数据获取,自动处理加载、错误状态,配合 Suspense 实现"等待- fallback"逻辑,消除模板代码;
  • useOptimistic():内置乐观更新逻辑,只需定义"当前状态"和"乐观更新函数",自动处理"临时状态展示"和"失败回滚",无需手动管理。

二、use() Hook 详解:异步数据的"极简使用方式"

use() 是 React 19 推出的"异步数据消费 Hook",核心作用是:直接"消费"Promise 对象,自动等待 Promise 决议(成功/失败),并将结果作为同步值返回。配合 Suspense 组件,就能轻松实现"加载中 fallback",彻底告别手动管理 loading/error 状态。

1. 基础用法:替代 useEffect + useState 获取异步数据

我们用 use() 重写上面的"用户信息获取"案例,看看代码能精简多少:

javascript 复制代码
// React 19 use() 实现异步数据获取
import { use } from 'react';

// 1. 定义异步请求函数(返回 Promise)
async function fetchUser(userId) {
  const res = await fetch(`/api/users/${userId}`);
  if (!res.ok) throw new Error('请求用户信息失败');
  return res.json();
}

// 2. 组件中使用 use() 消费 Promise
function UserProfile({ userId }) {
  // use() 直接接收 Promise,等待决议后返回数据
  const user = use(fetchUser(userId));

  // 无需手动处理 loading/error,由 Suspense 接管
  return (
    <div>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
}

// 3. 父组件用 Suspense 包裹,设置加载中 fallback
function App() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <UserProfile userId="123" />
    </Suspense>
  );
}

对比之前的代码,变化非常明显:

  • 去掉了 useState 管理的 loading、error、user 状态;
  • 去掉了 useEffect 触发请求的逻辑;
  • 加载中的状态由 Suspense 组件统一处理,只需设置 fallback
  • 错误处理:可以用 ErrorBoundary 组件统一捕获(下文会讲)。

2. 核心原理:use() 是如何工作的?

很多人会误以为 use() 是"将异步变成同步",但本质上,它是 配合 React 的 Suspense 机制,实现了"异步数据的同步式写法" 。其核心原理可以拆解为 3 步:

  1. 接收 Promise 并订阅:use() 接收一个 Promise 对象后,会立即订阅这个 Promise 的决议状态(成功/失败);
  2. 触发 Suspense 挂起:在 Promise 还未决议时,use() 会抛出一个"特殊的 Promise",这个 Promise 会被 React 捕获,从而触发 Suspense 组件的 fallback 渲染;
  3. Promise 决议后重新渲染:当 Promise 成功决议(resolve),use() 会返回决议后的值,组件重新渲染并展示数据;如果 Promise 失败(reject),会抛出错误,需要用 ErrorBoundary 捕获。

3. 进阶用法:错误处理与依赖更新

use() 本身不处理错误,需要配合 ErrorBoundary 组件捕获异步请求中的错误。同时,当 use() 依赖的参数变化时(如 userId 改变),会自动重新执行异步函数,触发新的请求。

案例 1:用 ErrorBoundary 处理错误

javascript 复制代码
// 1. 实现 ErrorBoundary 组件(捕获子组件错误)
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error }; // 捕获错误,更新状态
  }

  componentDidCatch(error, errorInfo) {
    console.error('请求错误:', error, errorInfo); // 日志上报
  }

  render() {
    if (this.state.hasError) {
      // 错误状态下的 UI
      return <div>加载失败:{this.state.error.message}</div>;
    }
    return this.props.children;
  }
}

// 2. 父组件组合 Suspense 和 ErrorBoundary
function App() {
  return (
    <ErrorBoundary>
      <Suspense fallback={<div>加载中...</div>}>
        <UserProfile userId="123" />
      </Suspense>
    </ErrorBoundary>
  );
}

案例 2:依赖参数变化自动更新

当 userId 变化时,use() 会自动重新调用 fetchUser,触发新的请求,无需手动用 useEffect 监听依赖:

ini 复制代码
function UserList() {
  const [selectedUserId, setSelectedUserId] = useState('123');

  return (
    <div>
      <div>
        <button onClick={() => setSelectedUserId('123')}>用户 123</button>
        <button onClick={() => setSelectedUserId('456')}>用户 456</button>
      </div>
      <Suspense fallback={<div>加载中...</div>}>
        <UserProfile userId={selectedUserId} /&gt;
      &lt;/Suspense&gt;
    &lt;/div&gt;
  );
}

4. 注意事项:use() 的使用限制

use() 虽然强大,但有几个关键限制,必须遵守,否则会导致组件行为异常:

  • 只能在组件顶层或自定义 Hook 顶层调用:不能在条件语句(if)、循环(for)、事件处理函数中调用(和其他 Hooks 一样,遵循 Hooks 规则);
  • 不能用于非 Suspense 环境:如果组件没有被 Suspense 包裹,use() 抛出的 Promise 会导致组件崩溃;
  • 不支持取消请求(需手动实现) :use() 本身不提供取消请求的能力,需要配合 AbortController 实现;
  • 不要用于同步值:use() 设计用于消费 Promise,传入同步值会导致不必要的性能开销。

补充:用 AbortController 取消请求

javascript 复制代码
async function fetchUser(userId, signal) {
  const res = await fetch(`/api/users/${userId}`, { signal }); // 传入信号
  if (!res.ok) throw new Error('请求用户信息失败');
  return res.json();
}

function UserProfile({ userId }) {
  // 创建 AbortController,组件卸载时取消请求
  const controller = useRef(new AbortController());
  useEffect(() => {
    return () => controller.current.abort(); // 组件卸载,取消请求
  }, []);

  // 传入 signal 给 fetchUser
  const user = use(fetchUser(userId, controller.current.signal));

  return (
    <div>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
}

三、useOptimistic() Hook 详解:乐观更新的"零成本实现"

useOptimistic() 是 React 19 专门为"乐观更新"场景设计的 Hook,核心作用是:在发送异步请求的同时,立即展示"预期的更新结果",等请求成功后确认状态,失败后自动回滚到之前的状态。无需手动管理临时状态和回滚逻辑,极大简化了乐观更新的实现。

1. 基础用法:重构"点赞功能"

我们用 useOptimistic() 重写之前的"点赞功能"案例,感受一下简化效果:

javascript 复制代码
import { useOptimistic } from 'react';

// 1. 异步请求函数(点赞接口)
async function likePost(postId) {
  const res = await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
  if (!res.ok) throw new Error('点赞失败');
  return res.json(); // 成功返回最新的点赞数
}

function LikeButton({ postId, initialLikes }) {
  // 2. 使用 useOptimistic:传入初始状态和乐观更新函数
  const [optimisticLikes, addLike] = useOptimistic(
    initialLikes, // 初始状态(真实的点赞数)
    (currentLikes) => currentLikes + 1 // 乐观更新函数:接收当前状态,返回预期状态
  );

  // 3. 点击事件:调用 addLike 触发乐观更新,同时发送请求
  const handleLike = async () => {
    // 触发乐观更新:立即更新 optimisticLikes 为 currentLikes + 1
    addLike();
    try {
      // 发送请求,等待真实结果
      const data = await likePost(postId);
      // 请求成功:无需额外操作,useOptimistic 会自动用真实结果同步状态
    } catch (err) {
      // 请求失败:无需手动回滚,useOptimistic 会自动恢复到之前的状态
      alert(err.message);
    }
  };

  return (
    <button onClick={handleLike}>
      点赞 ({optimisticLikes}) // 展示乐观更新后的状态
    </button>
  );
}

对比之前的实现,核心简化点:

  • 去掉了 useState 管理的 likes(真实状态)和 isPending(请求中状态);
  • 无需手动记录"之前的状态"用于回滚,useOptimistic 会自动管理;
  • 只需调用 addLike() 就能触发乐观更新,逻辑更集中。

2. 核心原理:useOptimistic() 是如何实现"自动回滚"的?

useOptimistic() 的核心逻辑是"维护两个状态":真实状态(realState)乐观状态(optimisticState) 。其工作流程可以拆解为 4 步:

  1. 初始化 :useOptimistic 接收 initialState 作为真实状态的初始值,乐观状态初始值与真实状态一致;
  2. 触发乐观更新:调用 useOptimistic 返回的更新函数(如 addLike)时,会根据传入的"乐观更新函数"计算出预期的乐观状态,并立即更新乐观状态,组件重新渲染,展示乐观结果;
  3. 等待异步请求:异步请求发送后,useOptimistic 会等待请求结果;
  4. 同步真实状态: - 若请求成功:用真实结果更新"真实状态",同时将"乐观状态"同步为真实状态(确保两者一致); - 若请求失败:放弃乐观状态,将"乐观状态"回滚到"真实状态",组件重新渲染,恢复之前的结果。

useOptimistic() 工作流程流程图

3. 进阶用法:复杂状态的乐观更新

useOptimistic() 不仅支持数字这种简单状态,也支持对象、数组等复杂状态。只需在"乐观更新函数"中正确处理复杂状态的变更即可。

案例:评论列表的乐观更新(复杂对象数组)

javascript 复制代码
import { useOptimistic, useState } from 'react';

// 异步请求:提交评论
async function submitComment(postId, comment) {
  const res = await fetch(`/api/posts/${postId}/comments`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(comment),
  });
  if (!res.ok) throw new Error('评论提交失败');
  return res.json(); // 成功返回包含 id 的完整评论
}

function CommentSection({ postId }) {
  const [content, setContent] = useState('');
  // 初始评论列表(空数组)
  const [comments, addComment] = useOptimistic(
    [],
    // 乐观更新函数:接收当前评论列表,返回预期的新列表
    (currentComments, newComment) => [
      ...currentComments,
      { ...newComment, id: 'temp-id' }, // 临时 id,请求成功后会替换
    ]
  );

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (!content.trim()) return;

    // 构建评论数据
    const newComment = { content, author: '我', time: new Date().toLocaleString() };
    // 触发乐观更新:传入新评论,更新评论列表
    addComment(newComment);
    // 清空输入框
    setContent('');

    try {
      // 发送请求,获取真实评论(包含真实 id)
      const realComment = await submitComment(postId, newComment);
      // 请求成功:useOptimistic 会自动用真实评论替换临时评论?不,需要手动同步!
      // 注意:复杂状态下,请求成功后需要手动更新真实状态
      // 这里的关键:addComment 只是触发乐观更新,真实状态需要我们手动维护?
      // 纠正:useOptimistic 的第二个参数是"乐观更新函数",接收的参数是(currentState, ...args)
      // 当调用 addComment(...args) 时,乐观状态 = 乐观更新函数(currentState, ...args)
      // 而真实状态的更新,需要我们在请求成功后,通过重新赋值 initialState 实现?
      // 正确的复杂状态用法:需要用 useState 管理真实状态,useOptimistic 基于真实状态做乐观更新
      // 重新梳理正确写法:
    } catch (err) {
      alert(err.message);
      // 失败无需手动回滚,useOptimistic 自动恢复
    }
  };

  // 正确的复杂状态写法:用 useState 管理真实状态,useOptimistic 包装
  const [realComments, setRealComments] = useState([]);
  const [optimisticComments, addOptimisticComment] = useOptimistic(
    realComments, // 基于真实状态
    (current, newComment) => [...current, { ...newComment, id: 'temp' }]
  );

  const handleSubmitCorrect = async (e) => {
    e.preventDefault();
    if (!content.trim()) return;
    const newComment = { content, author: '我', time: new Date().toLocaleString() };
    addOptimisticComment(newComment); // 乐观更新
    setContent('');
    try {
      const realComment = await submitComment(postId, newComment);
      setRealComments(prev => [...prev, realComment]); // 更新真实状态
    } catch (err) {
      alert(err.message);
    }
  };

  return (
    <div>
      <form onSubmit={handleSubmitCorrect}>
        <textarea
          value={content}
          onChange={(e) => setContent(e.target.value)}
          placeholder="输入评论..."
        />
        <button type="submit">提交评论</button>
      </form>
      <div className="comments">
        {optimisticComments.map(comment => (
          <div key={comment.id} className="comment">
            <p>{comment.author} {comment.time}</p>
            <p>{comment.content}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

复杂状态下的核心要点:

  • useState 管理"真实状态"(如 realComments);
  • useOptimistic 的第一个参数传入"真实状态",确保乐观更新基于最新的真实数据;
  • 请求成功后,通过 setRealComments 更新真实状态,useOptimistic 会自动同步乐观状态与真实状态;
  • 乐观更新函数中,可以用"临时 id"占位,请求成功后用真实 id 替换。

四、业务落地:5 个高频场景案例

了解了两个 Hooks 的基础用法和原理后,我们结合真实业务场景,看看它们在实际开发中如何落地。

场景 1:用户信息详情页(use() + Suspense + ErrorBoundary)

需求:进入页面加载用户信息,展示加载中状态,加载失败提示错误,支持切换用户。

javascript 复制代码
import { use, useState } from 'react';
import { Suspense } from 'react';

// 异步请求:获取用户信息
async function fetchUser(userId, signal) {
  const res = await fetch(`/api/users/${userId}`, { signal });
  if (!res.ok) throw new Error('获取用户信息失败');
  return res.json();
}

// 子组件:用户详情
function UserDetail({ userId }) {
  const controller = useRef(new AbortController());
  useEffect(() => {
    return () => controller.current.abort();
  }, []);
  const user = use(fetchUser(userId, controller.current.signal));
  return (
    <div className="user-detail">
      <img src={user.avatar} alt={user.name} className="avatar" />
      <h2>{user.name}</h2>
      <p>邮箱:{user.email}</p>
      <p>手机:{user.phone}</p>
      <p>地址:{user.address}</p>
    </div>
  );
}

// ErrorBoundary 组件(复用之前的实现)
class ErrorBoundary extends React.Component { /* ... */ }

// 父组件:用户列表 + 详情切换
function UserPage() {
  const [selectedUserId, setSelectedUserId] = useState('1');
  const userIds = ['1', '2', '3']; // 模拟用户列表

  return (
    <div className="user-page">
      <div className="user-list">
        {userIds.map(id => (
          <button
            key={id}
            onClick={() => setSelectedUserId(id)}
            className={selectedUserId === id ? 'active' : ''}
          >
            用户 {id}
          </button>
        ))}
      </div>
      <ErrorBoundary>
        <Suspense fallback={<div className="loading">加载用户信息中...</div>}>
          <UserDetail userId={selectedUserId} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

场景 2:商品收藏功能(useOptimistic + 接口请求)

需求:点击收藏按钮,立即显示"已收藏"状态,发送收藏请求,失败后回滚状态并提示。

javascript 复制代码
import { useOptimistic } from 'react';

// 异步请求:收藏/取消收藏商品
async function toggleCollect(goodsId, isCollected) {
  const res = await fetch(`/api/goods/${goodsId}/collect`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ isCollected: !isCollected }),
  });
  if (!res.ok) throw new Error(isCollected ? '取消收藏失败' : '收藏失败');
  return res.json();
}

function GoodsCollectButton({ goodsId, initialIsCollected }) {
  // 乐观更新:切换收藏状态
  const [isCollected, toggleOptimisticCollect] = useOptimistic(
    initialIsCollected,
    (current) => !current // 乐观更新函数:切换状态
  );

  const handleToggle = async () => {
    // 触发乐观更新
    toggleOptimisticCollect();
    try {
      // 发送请求
      await toggleCollect(goodsId, initialIsCollected);
      // 成功无需额外操作,若需要更新父组件状态,可通过回调函数
    } catch (err) {
      alert(err.message);
      // 失败自动回滚
    }
  };

  return (
    <button
      onClick={handleToggle}
      className={`collect-btn ${isCollected ? 'collected' : ''}`}
    >
      {isCollected ? '已收藏' : '收藏'}
      {isCollected ? '❤️' : '♡'}
    </button>
  );
}

场景 3:购物车数量修改(useOptimistic + 节流)

需求:点击"+""-"按钮修改购物车商品数量,立即更新数量,发送请求,失败回滚,防止重复点击。

javascript 复制代码
import { useOptimistic, useCallback } from 'react';
import { throttle } from 'lodash';

// 异步请求:修改购物车数量
async function updateCartItemCount(cartItemId, count) {
  const res = await fetch(`/api/cart/${cartItemId}`, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ count }),
  });
  if (!res.ok) throw new Error('修改数量失败');
  return res.json();
}

function CartItem({ item }) {
  const { id, name, price, count } = item;

  // 节流:防止频繁点击触发过多请求
  const throttledUpdate = useCallback(
    throttle(async (newCount) => {
      try {
        await updateCartItemCount(id, newCount);
      } catch (err) {
        alert(err.message);
      }
    }, 300),
    [id]
  );

  // 乐观更新:修改数量
  const [optimisticCount, updateOptimisticCount] = useOptimistic(
    count,
    (current, delta) => {
      const newCount = current + delta;
      return newCount <= 0 ? 1 : newCount; // 数量不小于 1
    }
  );

  const handleIncrement = () => {
    updateOptimisticCount(1); // 增加 1
    throttledUpdate(optimisticCount + 1);
  };

  const handleDecrement = () => {
    updateOptimisticCount(-1); // 减少 1
    throttledUpdate(optimisticCount - 1);
  };

  return (
    <div className="cart-item">
      <h4>{name}</h4>
      <p className="price">¥{price.toFixed(2)}</p>
      <div className="count-control">
        <button onClick={handleDecrement}>-</button>
        <span>{optimisticCount}</span>
        <button onClick={handleIncrement}>+</button>
      </div>
    </div>
  );
}

场景 4:表单提交乐观更新(useOptimistic + 复杂表单)

需求:提交表单后,立即显示"提交成功"的提示和表单内容,发送请求,失败后回滚提示并显示错误。

javascript 复制代码
import { useOptimistic, useState } from 'react';

// 异步请求:提交表单
async function submitForm(formData) {
  const res = await fetch('/api/form/submit', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(formData),
  });
  if (!res.ok) throw new Error('表单提交失败');
  return res.json();
}

function FormComponent() {
  const [name, setName] = useState('');
  const [age, setAge] = useState('');
  // 表单提交状态:{ status: 'idle' | 'submitted' | 'error', data: {} }
  const initialSubmitState = { status: 'idle', data: {} };

  // 乐观更新:提交后立即显示成功状态
  const [submitState, submitOptimisticForm] = useOptimistic(
    initialSubmitState,
    (current, formData) => ({
      status: 'submitted',
      data: formData,
    })
  );

  const handleSubmit = async (e) => {
    e.preventDefault();
    const formData = { name, age };
    // 触发乐观更新:显示提交成功状态
    submitOptimisticForm(formData);
    try {
      // 发送请求
      await submitForm(formData);
      // 成功无需额外操作,若需要重置表单,可在这里处理
      setName('');
      setAge('');
    } catch (err) {
      // 失败自动回滚到 idle 状态
      alert(err.message);
    }
  };

  return (
    <div className="form-container">
      <form onSubmit={handleSubmit}>
        <div className="form-item">
          <label>姓名:</label>
          <input
            type="text"
            value={name}
            onChange={(e) => setName(e.target.value)}
            required
          />
        </div>
        <div className="form-item">
          <label>年龄:</label>
          <input
            type="number"
            value={age}
            onChange={(e) => setAge(e.target.value)}
            required
          />
        </div>
        <button type="submit" disabled={submitState.status === 'submitted'}>
          提交
        </button>
      </form>

      // 乐观更新后的提示
      {submitState.status === 'submitted' && (
        <div className="submit-success">
          <h3>提交成功!</h3>
          <p>姓名:{submitState.data.name}</p>
          <p>年龄:{submitState.data.age}</p>
        </div>
      )}
    </div>
  );
}

场景 5:use() 配合自定义 Hook 封装异步请求

需求:封装一个通用的异步请求 Hook,统一处理请求、取消、错误捕获,供多个组件复用。

javascript 复制代码
import { use, useRef, useEffect } from 'react';

// 自定义 Hook:useAsyncRequest,封装 use() 的异步请求逻辑
function useAsyncRequest(requestFn) {
  // 创建 AbortController,用于取消请求
  const controller = useRef(new AbortController());

  // 组件卸载时取消请求
  useEffect(() => {
    return () => controller.current.abort();
  }, []);

  // 执行请求并返回结果
  const execute = (...args) => {
    return requestFn(...args, controller.current.signal);
  };

  // 返回 execute 函数和 use() 消费的结果(如果需要立即执行)
  return { execute, useResult: (...args) => use(execute(...args)) };
}

// 使用自定义 Hook 封装用户请求
async function fetchUser(userId, signal) {
  const res = await fetch(`/api/users/${userId}`, { signal });
  if (!res.ok) throw new Error('获取用户信息失败');
  return res.json();
}

// 组件中使用自定义 Hook
function UserProfile({ userId }) {
  const { useResult } = useAsyncRequest(fetchUser);
  const user = useResult(userId); // 直接使用结果

  return (
    <div>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </div>
  );
}

// 另一个组件:延迟执行请求(比如点击按钮后)
function UserSearch() {
  const [userId, setUserId] = useState('');
  const { execute } = useAsyncRequest(fetchUser);
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);

  const handleSearch = async () => {
    if (!userId) return;
    setLoading(true);
    try {
      const data = await execute(userId);
      setUser(data);
    } catch (err) {
      alert(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      <input
        type="text"
        value={userId}
        onChange={(e) => setUserId(e.target.value)}
        placeholder="输入用户 ID"
      />
      <button onClick={handleSearch} disabled={loading}>
        搜索
      </button>
      {loading && <div>搜索中...</div>}
      {user && (
        <div>
          <h3>{user.name}</h3>
          <p>{user.email}</p>
        </div>
      )}
    </div>
  );
}

五、避坑指南:使用 use()/useOptimistic 的 6 个关键注意点

在实际使用过程中,有几个容易踩坑的点,需要特别注意:

1. use() 必须配合 Suspense 使用

如果组件没有被 Suspense 包裹,use() 抛出的 Promise 会导致组件崩溃。即使你不需要加载中状态,也必须用 Suspense 包裹(可以设置 fallback 为 null)。

2. use() 不支持在事件处理函数中调用

use() 必须在组件顶层调用,不能在 onClick、onSubmit 等事件处理函数中使用。如果需要"点击后再请求数据",可以用 useRef 存储 AbortController,在事件处理函数中手动执行异步函数,再用 useState 管理结果。

3. useOptimistic() 的乐观状态是"临时的"

useOptimistic() 的乐观状态只在当前组件渲染周期内有效,组件卸载后会丢失。如果需要持久化状态,还是需要用 useState 或 Redux 等状态管理库管理真实状态。

4. useOptimistic() 不处理请求并发问题

如果短时间内触发多次乐观更新(比如快速点击点赞按钮),会导致多个异步请求并发,可能出现状态混乱。需要配合节流、防抖或请求队列管理,避免并发问题。

5. 错误处理必须用 ErrorBoundary

use() 和 useOptimistic() 都不自带错误处理能力,异步请求的错误必须用 ErrorBoundary 组件捕获,否则会导致整个应用崩溃。

6. useOptimistic() 的乐观更新函数必须是纯函数

乐观更新函数(第二个参数)不能有副作用(比如修改全局变量、发送请求),必须是纯函数,只依赖当前状态和传入的参数,否则会导致状态不一致。

六、核心总结

今天我们详细拆解了 React 19 新 Hooks use() 和 useOptimistic() 的核心原理和业务落地方法,核心要点总结如下:

  1. use() 的核心价值:简化异步数据获取,自动处理加载状态,配合 Suspense 和 ErrorBoundary 消除模板代码;
  2. use() 的原理:订阅 Promise,未决议时触发 Suspense 挂起,决议后返回结果或抛出错误;
  3. useOptimistic() 的核心价值:零成本实现乐观更新,自动管理乐观状态和真实状态,失败后自动回滚;
  4. useOptimistic() 的原理:维护真实状态和乐观状态,触发更新时先更新乐观状态,请求完成后同步真实状态;
  5. 落地关键:use() 必须配合 Suspense 和 ErrorBoundary,useOptimistic() 复杂状态需结合 useState 管理真实状态,避免并发和副作用问题。

七、进阶学习方向

掌握了这两个 Hooks 的基础用法后,可以进一步学习以下内容,深化理解:

  • use() 与 Suspense Data Fetching 的关系:React 19 对数据获取的整体优化思路;
  • useOptimistic() 与并发渲染的协同:在并发模式下如何保证乐观更新的稳定性;
  • React 19 其他新特性:如 Action、Server Components 等,如何与 use()/useOptimistic() 配合使用;
  • 源码阅读:查看 React 源码中 use() 和 useOptimistic() 的实现,深入理解底层逻辑。

如果这篇文章对你有帮助,欢迎点赞、收藏、转发~ 有任何问题也可以在评论区留言交流~ 我们下期再见!

相关推荐
奔跑的web.2 小时前
Vue 3.6 重磅新特性:Vapor Mode 深度解析
前端·javascript·vue.js
唐璜Taro2 小时前
2026全栈开发AI智能体教程(开篇一)
javascript·langchain
爱敲代码的婷婷婷.2 小时前
patch-package 修改 node_modules流程以及注意点
前端·react native·前端框架·node.js
这是个栗子2 小时前
【API封装参数传递】params 与 API 封装
开发语言·前端·javascript·data·params
凌栀茗2 小时前
html第二天
前端·javascript·html
Amumu121382 小时前
Redux介绍(一)
前端·javascript·react.js
旭日猎鹰2 小时前
配置ReactNative环境并创建第一个程序
javascript·react native·react.js
麷飞花2 小时前
TypeScript问题
前端·javascript·vscode·typescript·ts
阿湯哥2 小时前
ReAct智能体
前端·react.js·前端框架