useOptimistic

10 小时前
/ ,
1

useOptimistic

useOptimistic 允许在进行异步操作时显示一个临时state。它接受一个state输入,并通过一个 updateFn 来更新临时的state,此更新通常被成为乐观更新,待异步操作完成时触发set原state,原state 更新触发重渲染,React 会用新 state 重新执行 useOptimistic,此时乐观值被丢弃,UI 回到真实值。

使用场景

useOptimistic 用于“用户触发了一个会走异步/服务端的动作,但我希望 UI 立刻给出成功后的预期状态”,也就是先乐观展示、失败再回滚/提示,从而让交互更顺滑。

它适合“高成功率、可回滚”的操作;如果失败代价很大或强一致性要求高(比如扣款/下单库存),不建议纯乐观,至少要更谨慎的确认与补偿策略。 需要考虑失败处理:回滚、toast、重试、幂等(避免重复插入/重复点赞)等。

应用场景

  • 表单提交/评论发送/聊天发消息:点击发送后马上把消息插入列表并标记 sending...,成功后去掉标记;失败则标红、显示重试按钮或撤回。
  • 点赞/收藏/关注/投票:点一次立刻让计数 +1/按钮变已赞,后台请求完成后确认;失败则撤销并提示“操作失败,请重试”。
  • 列表的增删改(本地先改列表,服务端后落库):例如待办事项“勾选完成/删除/新增”,先更新列表让用户看到变化,再等待接口返回。 轻量级设置切换:例如“开启通知/深色模式偏好同步到服务器”,先切 UI 开关状态,接口失败再恢复
import { useOptimistic, useState, useRef } from "react";
/**
 * 当原 state 更新(比如 setMessages((prev) => [...prev, ...]))时,optimisticMessages 会自动更新吗?
 * 
 * 会的。因为 optimisticMessages 不是独立存储,它是由 messages + optimistic 队列计算出来的派生结果。setMessages 触发重新渲染后,useOptimistic 会用新的 messages 重新计算 optimisticMessages。
 */
type Message = {
    id: string;
    text: string;
    sending: boolean;
}

export async function deliverMessage(text: string) {
  await new Promise((res) => setTimeout(res, 1000));
  return text;
}

export default function UseOptimisticDemo1() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [optimisticMessages, addOptimisticMessage] = useOptimistic<Message[], Message>(
    messages,
    (state, newMessage) => {
      if (state.some((m) => m.id === newMessage.id)) return state;
      return [...state, newMessage];
    }
  );
  const form = useRef<HTMLFormElement>(null);

  async function formAction(formData: FormData) {
    const raw = formData.get("message");
    if (typeof raw !== "string") return;
    const text = raw.trim();
    if (!text) return;

    const optimisticMessage: Message = {
      id: crypto.randomUUID(),
      text,
      sending: true,
    };

    addOptimisticMessage(optimisticMessage);
    form.current?.reset();

    const deliveredText = await deliverMessage(text);
    setMessages((prev) => [...prev, { ...optimisticMessage, text: deliveredText, sending: false }]);
  }

  return (
    <>
      <form action={formAction} ref={form}>
        <input
          type="text"
          name="message"
          placeholder="enter your message"
        />
        <button
          type="submit"
          style={{marginLeft: '10px'}}
        >Send</button>
      </form>

      {optimisticMessages.map((message, index) => (
        <div key={message.id}>
          {message.text}
          {!!message.sending && <small> (Sending...)</small>}
        </div>
      ))}
    </>
  );
}

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...