知海

useOptimistic()

ReactAPI 参考:React 核心

useOptimistic()

useOptimistic 是一个 React Hook,它可以帮助你更乐观地更新用户界面。

js 复制代码
const [optimisticState, setOptimistic] = useOptimistic(value, reducer?);

参考 {/reference/}

useOptimistic(value, reducer?) {/useoptimistic/}

在组件的顶层调用 useOptimistic 来创建一个乐观状态。

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

function MyComponent({name, todos}) {
  const [optimisticAge, setOptimisticAge] = useOptimistic(28);
  const [optimisticName, setOptimisticName] = useOptimistic(name);
  const [optimisticTodos, setOptimisticTodos] = useOptimistic(todos, todoReducer);
  // ...
}

参阅下方更多示例

参数 {/parameters/}

  • value:当没有待处理的 Action 时返回的值。
  • 可选 reducer(currentState, action):指定乐观状态如何更新的 reducer 函数。它必须是纯函数,接收当前状态和 reducer 操作的参数,并返回下一个乐观状态。

返回值 {/returns/}

useOptimistic 返回一个包含两个值的数组:

  1. optimisticState:当前的乐观状态。如果没有待处理的 Action,它等于 value;否则等于 reducer 返回的状态(如果没有提供 reducer,则等于传入 set 函数的值)。
  2. set 函数:允许你在 Action 内部将乐观状态更新为不同的值。

set 函数,如 setOptimistic(optimisticState) {/setoptimistic/}

useOptimistic 返回的 set 函数允许你为 Action 的持续时间内更新状态。你可以直接传递下一个状态,或传入一个基于前一状态计算新状态的函数:

js 复制代码
const [optimisticLike, setOptimisticLike] = useOptimistic(false);
const [optimisticSubs, setOptimisticSubs] = useOptimistic(subs);

function handleClick() {
  startTransition(async () => {
    setOptimisticLike(true);
    setOptimisticSubs(a => a + 1);
    await saveChanges();
  });
}

参数 {/setoptimistic-parameters/}

  • optimisticState:你希望在 Action 期间乐观状态成为的值。如果你为 useOptimistic 提供了 reducer,这个值将作为第二个参数传递给 reducer。它可以是任何类型的值。
    • 如果你将一个函数作为 optimisticState 传入,它将被视为_更新函数_。它必须是纯函数,只接收待处理状态作为其唯一参数,并返回下一个乐观状态。React 会将你的更新函数放入队列,并重新渲染组件。在下一次渲染期间,React 会将排队的更新函数应用到上一个状态来计算下一个状态,类似于 useState 更新函数

返回值 {/setoptimistic-returns/}

set 函数没有返回值。

注意事项 {/setoptimistic-caveats/}

  • set 函数必须在 Action 内部调用。如果你在 Action 外部调用 setter,React 将显示警告,乐观状态会短暂渲染。

乐观状态如何工作 {/how-optimistic-state-works/}

useOptimistic 允许你在 Action 进行期间显示临时值:

js 复制代码
const [value, setValue] = useState('a');
const [optimistic, setOptimistic] = useOptimistic(value);

startTransition(async () => {
  setOptimistic('b');
  const newValue = await saveChanges('b');
  setValue(newValue);
});

当 setter 在 Action 内被调用时,useOptimistic 会触发一次重新渲染,在 Action 进行期间显示该状态。否则,返回传入 useOptimisticvalue

这种状态被称为“乐观的”,是因为它被用来立即向用户展示执行 Action 的结果,即使该 Action 实际上需要时间才能完成。

更新流程

  1. 立即更新:当调用 setOptimistic('b') 时,React 立即以 'b' 渲染。
  2. (可选)在 Action 中等待:如果你在 Action 中 await,React 会继续显示 'b'
  3. 调度 TransitionsetValue(newValue) 调度一个对真实状态的更新。
  4. (可选)等待 Suspense:如果 newValue 挂起,React 会继续显示 'b'
  5. 一次渲染提交:最后,newValue 提交到 valueoptimistic

没有额外的渲染来“清除”乐观状态。当 Transition 完成时,乐观状态和真实状态会在同一次渲染中收敛。

乐观状态是临时的 {/optimistic-state-is-temporary/}

乐观状态只在 Action 进行期间渲染,否则渲染 value

如果 saveChanges 返回了 'c',那么 valueoptimistic 都将变为 'c',而不是 'b'

最终状态如何确定

useOptimisticvalue 参数决定了 Action 结束后显示的内容。其工作方式取决于你使用的模式:

  • 硬编码值,如 useOptimistic(false):Action 结束后,state 仍然是 false,所以 UI 显示 false。这对于总是从 false 开始的待处理状态很有用。
  • 传入 props 或 state,如 useOptimistic(isLiked):如果父组件在 Action 期间更新了 isLiked,Action 完成后会使用新值。这就是 UI 反映 Action 结果的方式。
  • reducer 模式,如 useOptimistic(items, fn):如果 items 在 Action 待处理时发生变化,React 会使用新的 items 重新运行 reducer 来重新计算状态。这会使你的乐观添加保持基于最新数据之上。

Action 失败时会发生什么

如果 Action 抛出错误,Transition 仍然结束,React 会以当前 value 进行渲染。由于父组件通常只在成功时更新 value,失败意味着 value 没有改变,因此 UI 显示乐观更新之前的内容。你可以捕获错误并向用户显示消息。


用法 {/usage/}

向组件添加乐观状态 {/adding-optimistic-state-to-a-component/}

在组件顶层调用 useOptimistic 来声明一个或多个乐观状态。

js [[1, 4, "age"], [1, 5, "name"], [1, 6, "todos"], [2, 4, "optimisticAge"], [2, 5, "optimisticName"], [2, 6, "optimisticTodos"], [3, 4, "setOptimisticAge"], [3, 5, "setOptimisticName"], [3, 6, "setOptimisticTodos"], [4, 6, "reducer"]] 复制代码
import { useOptimistic } from 'react';

function MyComponent({age, name, todos}) {
  const [optimisticAge, setOptimisticAge] = useOptimistic(age);
  const [optimisticName, setOptimisticName] = useOptimistic(name);
  const [optimisticTodos, setOptimisticTodos] = useOptimistic(todos, reducer);
  // ...

useOptimistic 返回一个包含两项的数组:

  1. 乐观状态,初始设置为提供的 value
  2. set 函数,允许你在 Action 期间临时更改状态。
    • 如果提供了 reducer,它会在返回乐观状态之前运行。

要使用 乐观状态,请在 Action 内部调用 set 函数。

Action 是在 startTransition 内部调用的函数:

js {3} 复制代码
function onAgeChange(e) {
  startTransition(async () => {
    setOptimisticAge(42);
    const newAge = await postAge(42);
    setAge(newAge);
  });
}

React 会先渲染乐观状态 42,而 age 保持当前年龄。Action 等待 POST,然后为 ageoptimisticAge 渲染 newAge

参见 乐观状态如何工作 深入了解。

当使用 Action props 时,你可以无需 startTransition 直接调用 set 函数:

js [[3, 2, "setOptimisticName"]] 复制代码
async function submitAction() {
  setOptimisticName('Taylor');
  await updateName('Taylor');
}

这是因为 Action props 已经在 startTransition 内部被调用。

有关示例,请参阅:在 Action props 中使用乐观状态


在 Action props 中使用乐观状态 {/using-optimistic-state-in-action-props/}

Action prop 中,你可以直接调用乐观 setter,无需 startTransition

此示例在 <form>submitAction prop 中设置乐观状态:

js src/App.js 复制代码
import { useState, startTransition } from 'react';
import EditName from './EditName';

export default function App() {
  const [name, setName] = useState('Alice');

  return <EditName name={name} action={setName} />;
}
js src/EditName.js active 复制代码
import { useOptimistic, startTransition } from 'react';
import { updateName } from './actions.js';

export default function EditName({ name, action }) {
  const [optimisticName, setOptimisticName] = useOptimistic(name);

  async function submitAction(formData) {
    const newName = formData.get('name');
    setOptimisticName(newName);

    const updatedName = await updateName(newName);
    startTransition(() => {
      action(updatedName);
    })
  }

  return (
    <form action={submitAction}>
      <p>你的名字是:{optimisticName}</p>
      <p>
        <label>修改:</label>
        <input
          type="text"
          name="name"
          disabled={name !== optimisticName}
        />
      </p>
    </form>
  );
}
js src/actions.js hidden 复制代码
export async function updateName(name) {
  await new Promise((res) => setTimeout(res, 1000));
  return name;
}

在此示例中,当用户提交表单时,optimisticName 会立即更新,以在服务器请求进行期间乐观地显示 newName。当请求完成时,nameoptimisticName 会使用响应中的实际 updatedName 渲染。

为什么这里不需要 startTransition? {/why-doesnt-this-need-starttransition/}

按照约定,在 startTransition 内部调用的 props 会被命名为“Action”。

因为 submitAction 以“Action”命名,你可以知道它已经在 startTransition 内部被调用。

参见 从组件暴露 action prop 了解 Action prop 模式。


向 Action props 添加乐观状态 {/adding-optimistic-state-to-action-props/}

在创建 Action prop 时,你可以添加 useOptimistic 来显示即时反馈。

这里有一个按钮,在 action 待处理时显示“提交中...”:

js src/App.js 复制代码
import { useState, startTransition } from 'react';
import Button from './Button';
import { submitForm } from './actions.js';

export default function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <Button action={async () => {
        await submitForm();
        startTransition(() => {
          setCount(c => c + 1);
        });
      }}>增加</Button>
      {count > 0 && <p>已提交 {count} 次!</p>}
    </div>
  );
}
js src/Button.js active 复制代码
import { useOptimistic, startTransition } from 'react';

export default function Button({ action, children }) {
  const [isPending, setIsPending] = useOptimistic(false);

  return (
    <button
      disabled={isPending}
      onClick={() => {
        startTransition(async () => {
          setIsPending(true);
          await action();
        });
      }}
    >
      {isPending ? '提交中...' : children}
    </button>
  );
}
js src/actions.js hidden 复制代码
export async function submitForm() {
  await new Promise((res) => setTimeout(res, 1000));
}

当按钮被点击时,setIsPending(true) 使用乐观状态立即显示“提交中...”并禁用按钮。当 Action 完成时,isPending 会自动渲染为 false

此模式会自动为 Button 的任何 action prop 显示待处理状态:

js 复制代码
// 显示状态更新的待处理状态
<Button action={() => { setState(c => c + 1) }} />

// 显示导航的待处理状态
<Button action={() => { navigate('/done') }} />

// 显示 POST 请求的待处理状态
<Button action={async () => { await fetch(/* ... */) }} />

// 显示组合操作的待处理状态
<Button action={async () => {
  setState(c => c + 1);
  await fetch(/* ... */);
  navigate('/done');
}} />

待处理状态会一直显示,直到 action prop 中的所有操作完成。

你也可以使用 useTransition 通过 isPending 获取待处理状态。

区别在于 useTransition 返回 startTransition 函数,而 useOptimistic 适用于任何 Transition。根据你组件的需求选择使用。


乐观地更新 props 或 state {/updating-props-or-state-optimistically/}

你可以将 props 或 state 包装在 useOptimistic 中,以便在 Action 进行期间立即更新它。

在此示例中,LikeButton 接收 isLiked 作为 prop,并在点击时立即切换它:

js src/App.js 复制代码
import { useState, useOptimistic, startTransition } from 'react';
import { toggleLike } from './actions.js';

export default function App() {
  const [isLiked, setIsLiked] = useState(false);
  const [optimisticIsLiked, setOptimisticIsLiked] = useOptimistic(isLiked);

  function handleClick() {
    startTransition(async () => {
      const newValue = !optimisticIsLiked
      console.log('⏳ 设置乐观状态:' + newValue);

      setOptimisticIsLiked(newValue);
      const updatedValue = await toggleLike(newValue);

      startTransition(() => {
        console.log('⏳ 设置真实状态:' + updatedValue );
        setIsLiked(updatedValue);
      });
    });
  }

  if (optimisticIsLiked !== isLiked) {
    console.log('✅ 渲染乐观状态:' + optimisticIsLiked);
  } else {
    console.log('✅ 渲染真实值:' + optimisticIsLiked);
  }


  return (
    <button onClick={handleClick}>
      {optimisticIsLiked ? '❤️ 取消喜欢' : '🤍 喜欢'}
    </button>
  );
}
js src/actions.js hidden 复制代码
export async function toggleLike(value) {
  return await new Promise((res) => setTimeout(() => res(value), 1000));
  // 在真实应用中,这会更新服务器
}
js src/index.js hidden 复制代码
import React from 'react';
import {createRoot} from 'react-dom/client';
import './styles.css';

import App from './App';

const root = createRoot(document.getElementById('root'));
// 未使用 StrictMode,因此不会显示双重渲染日志。
root.render(<App />);

当按钮被点击时,setOptimisticIsLiked 立即更新显示的状态,将爱心显示为已喜欢。与此同时,await toggleLike 在后台运行。当 await 完成时,父组件的 setIsLiked 更新“真实”的 isLiked 状态,乐观状态也会渲染以匹配这个新值。

此示例读取 optimisticIsLiked 来计算下一个值。这适用于基础状态不会改变的情况,但如果你的基础状态在 Action 待处理时可能发生变化,你可能需要使用状态更新函数或 reducer。

参见 基于当前状态更新状态 查看示例。


一起更新多个值 {/updating-multiple-values-together/}

当乐观更新影响多个相关值时,使用 reducer 一起更新它们。这可以确保 UI 保持一致。

这是一个关注按钮,同时更新关注状态和粉丝数量:

js src/App.js 复制代码
import { useState, startTransition } from 'react';
import { followUser, unfollowUser } from './actions.js';
import FollowButton from './FollowButton';

export default function App() {
  const [user, setUser] = useState({
    name: 'React',
    isFollowing: false,
    followerCount: 10500
  });

  async function followAction(shouldFollow) {
    if (shouldFollow) {
      await followUser(user.name);
    } else {
      await unfollowUser(user.name);
    }
    startTransition(() => {
      setUser(current => ({
        ...current,
        isFollowing: shouldFollow,
        followerCount: current.followerCount + (shouldFollow ? 1 : -1)
      }));
    });
  }

  return <FollowButton user={user} followAction={followAction} />;
}
js src/FollowButton.js active 复制代码
import { useOptimistic, startTransition } from 'react';

export default function FollowButton({ user, followAction }) {
  const [optimisticState, updateOptimistic] = useOptimistic(
    { isFollowing: user.isFollowing, followerCount: user.followerCount },
    (current, isFollowing) => ({
      isFollowing,
      followerCount: current.followerCount + (isFollowing ? 1 : -1)
    })
  );

  function handleClick() {
    const newFollowState = !optimisticState.isFollowing;
    startTransition(async () => {
      updateOptimistic(newFollowState);
      await followAction(newFollowState);
    });
  }

  return (
    <div>
      <p><strong>{user.name}</strong></p>
      <p>{optimisticState.followerCount} 粉丝</p>
      <button onClick={handleClick}>
        {optimisticState.isFollowing ? '取消关注' : '关注'}
      </button>
    </div>
  );
}
js src/actions.js hidden 复制代码
export async function followUser(name) {
  await new Promise((res) => setTimeout(res, 1000));
}

export async function unfollowUser(name) {
  await new Promise((res) => setTimeout(res, 1000));
}

reducer 接收新的 isFollowing 值,并在单次更新中同时计算新的关注状态和更新后的粉丝数量。这确保了按钮文本和数量始终同步。

在更新函数和 reducer 之间选择 {/choosing-between-updaters-and-reducers/}

useOptimistic 支持两种基于当前状态计算状态的模式:

更新函数的工作方式类似于 useState 更新函数。将一个函数传递给 setter:

js 复制代码
const [optimistic, setOptimistic] = useOptimistic(value);
setOptimistic(current => !current);

Reducers 将更新逻辑与 setter 调用分开:

js 复制代码
const [optimistic, dispatch] = useOptimistic(value, (current, action) => {
  // 基于 current 和 action 计算下一个状态
});
dispatch(action);

当 setter 调用自然地描述更新时,使用更新函数。 这类似于使用 useState 时使用 setState(prev => ...)

当需要向更新传递数据(例如要添加的项)或需要在单个 hook 中处理多种更新类型时,使用 reducers。

为什么使用 reducer?

当你的 Transition 待处理期间基础状态可能发生变化时,reducers 是必不可少的。如果你的 todos 在添加待处理期间发生变化(例如,另一个用户添加了一个待办),React 会使用新的 todos 重新运行你的 reducer 来重新计算显示内容。这确保你的新待办被添加到最新列表,而不是过时的副本。

setOptimistic(prev => [...prev, newItem]) 这样的更新函数只会看到 Transition 开始时的状态,而会遗漏异步工作期间发生的任何更新。


乐观地添加到列表 {/optimistically-adding-to-a-list/}

当你需要乐观地向列表中添加项目时,使用 reducer

js src/App.js 复制代码
import { useState, startTransition } from 'react';
import { addTodo } from './actions.js';
import TodoList from './TodoList';

export default function App() {
  const [todos, setTodos] = useState([
    { id: 1, text: '学习 React' }
  ]);

  async function addTodoAction(newTodo) {
    const savedTodo = await addTodo(newTodo);
    startTransition(() => {
      setTodos(todos => [...todos, savedTodo]);
    });
  }

  return <TodoList todos={todos} addTodoAction={addTodoAction} />;
}
js src/TodoList.js active 复制代码
import { useOptimistic, startTransition } from 'react';

export default function TodoList({ todos, addTodoAction }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (currentTodos, newTodo) => [
      ...currentTodos,
      { id: newTodo.id, text: newTodo.text, pending: true }
    ]
  );

  function handleAddTodo(text) {
    const newTodo = { id: crypto.randomUUID(), text: text };
    startTransition(async () => {
      addOptimisticTodo(newTodo);
      await addTodoAction(newTodo);
    });
  }

  return (
    <div>
      <button onClick={() => handleAddTodo('新待办')}>添加待办</button>
      <ul>
        {optimisticTodos.map(todo => (
          <li key={todo.id}>
            {todo.text} {todo.pending && "(添加中...)"}
          </li>
        ))}
      </ul>
    </div>
  );
}
js src/actions.js hidden 复制代码
export async function addTodo(todo) {
  await new Promise((res) => setTimeout(res, 1000));
  // 在真实应用中,这会保存到服务器
  return { ...todo, pending: false };
}

reducer 接收当前待办列表和新添加的待办。这一点很重要,因为如果你的 todos prop 在添加待处理期间发生变化(例如,另一个用户添加了一个待办),React 会通过使用更新后的列表重新运行 reducer 来更新你的乐观状态。这确保你的新待办被添加到最新列表,而不是过时的副本。

每个乐观项包含一个 pending: true 标记,以便你可以为单个项显示加载状态。当服务器响应并且父组件用已保存的项更新规范的 todos 列表时,乐观状态会更新为已确认的项,而不带 pending 标记。


处理多种 action 类型 {/handling-multiple-action-types/}

当你需要处理多种类型的乐观更新(如添加和移除项目)时,使用 reducer 模式和 action 对象。

此购物车示例展示了如何使用一个 reducer 处理添加和移除:

js src/App.js 复制代码
import { useState, startTransition } from 'react';
import { addToCart, removeFromCart, updateQuantity } from './actions.js';
import ShoppingCart from './ShoppingCart';

export default function App() {
  const [cart, setCart] = useState([]);

  const cartActions = {
    async add(item) {
      await addToCart(item);
      startTransition(() => {
        setCart(current => {
          const exists = current.find(i => i.id === item.id);
          if (exists) {
            return current.map(i =>
              i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
            );
          }
          return [...current, { ...item, quantity: 1 }];
        });
      });
    },
    async remove(id) {
      await removeFromCart(id);
      startTransition(() => {
        setCart(current => current.filter(item => item.id !== id));
      });
    },
    async updateQuantity(id, quantity) {
      await updateQuantity(id, quantity);
      startTransition(() => {
        setCart(current =>
          current.map(item =>
            item.id === id ? { ...item, quantity } : item
          )
        );
      });
    }
  };

  return <ShoppingCart cart={cart} cartActions={cartActions} />;
}
js src/ShoppingCart.js active 复制代码
import { useOptimistic, startTransition } from 'react';

export default function ShoppingCart({ cart, cartActions }) {
  const [optimisticCart, dispatch] = useOptimistic(
    cart,
    (currentCart, action) => {
      switch (action.type) {
        case 'add':
          const exists = currentCart.find(item => item.id === action.item.id);
          if (exists) {
            return currentCart.map(item =>
              item.id === action.item.id
                ? { ...item, quantity: item.quantity + 1, pending: true }
                : item
            );
          }
          return [...currentCart, { ...action.item, quantity: 1, pending: true }];
        case 'remove':
          return currentCart.filter(item => item.id !== action.id);
        case 'update_quantity':
          return currentCart.map(item =>
            item.id === action.id
              ? { ...item, quantity: action.quantity, pending: true }
              : item
          );
        default:
          return currentCart;
      }
    }
  );

  function handleAdd(item) {
    startTransition(async () => {
      dispatch({ type: 'add', item });
      await cartActions.add(item);
    });
  }

  function handleRemove(id) {
    startTransition(async () => {
      dispatch({ type: 'remove', id });
      await cartActions.remove(id);
    });
  }

  function handleUpdateQuantity(id, quantity) {
    startTransition(async () => {
      dispatch({ type: 'update_quantity', id, quantity });
      await cartActions.updateQuantity(id, quantity);
    });
  }

  const total = optimisticCart.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );

  return (
    <div>
      <h2>购物车</h2>
      <div style={{ marginBottom: 16 }}>
        <button onClick={() => handleAdd({
          id: 1, name: 'T 恤', price: 25
        })}>
          添加 T 恤($25)
        </button>{' '}
        <button onClick={() => handleAdd({
          id: 2, name: '马克杯', price: 15
        })}>
          添加马克杯($15)
        </button>
      </div>
      {optimisticCart.length === 0 ? (
        <p>你的购物车是空的</p>
      ) : (
        <ul>
          {optimisticCart.map(item => (
            <li key={item.id}>
              {item.name} - ${item.price} ×
              {item.quantity}
              {' '}= ${item.price * item.quantity}
              <button
                onClick={() => handleRemove(item.id)}
                style={{ marginLeft: 8 }}
              >
                移除
              </button>
              {item.pending && ' ...'}
            </li>
          ))}
        </ul>
      )}
      <p><strong>总计:${total}</strong></p>
    </div>
  );
}
js src/actions.js hidden 复制代码
export async function addToCart(item) {
  await new Promise((res) => setTimeout(res, 800));
}

export async function removeFromCart(id) {
  await new Promise((res) => setTimeout(res, 800));
}

export async function updateQuantity(id, quantity) {
  await new Promise((res) => setTimeout(res, 800));
}

reducer 处理三种 action 类型(addremoveupdate_quantity),并为每种类型返回新的乐观状态。每个 action 都会设置一个 pending: true 标记,以便你可以显示视觉反馈,同时 Server Function 正在运行。


带有错误恢复的乐观删除 {/optimistic-delete-with-error-recovery/}

当乐观地删除项目时,你应该处理 Action 失败的情况。

此示例展示了当删除失败时如何显示错误消息,并且 UI 会自动回滚以再次显示该项目。

js src/App.js 复制代码
import { useState, startTransition } from 'react';
import { deleteItem } from './actions.js';
import ItemList from './ItemList';

export default function App() {
  const [items, setItems] = useState([
    { id: 1, name: '学习 React' },
    { id: 2, name: '构建应用' },
    { id: 3, name: '部署到生产环境' },
  ]);

  async function deleteAction(id) {
    await deleteItem(id);
    startTransition(() => {
      setItems(current => current.filter(item => item.id !== id));
    });
  }

  return <ItemList items={items} deleteAction={deleteAction} />;
}
js src/ItemList.js active 复制代码
import { useState, useOptimistic, startTransition } from 'react';

export default function ItemList({ items, deleteAction }) {
  const [error, setError] = useState(null);
  const [optimisticItems, removeItem] = useOptimistic(
    items,
    (currentItems, idToRemove) =>
      currentItems.map(item =>
        item.id === idToRemove
          ? { ...item, deleting: true }
          : item
      )
  );

  function handleDelete(id) {
    setError(null);
    startTransition(async () => {
      removeItem(id);
      try {
        await deleteAction(id);
      } catch (e) {
        setError(e.message);
      }
    });
  }

  return (
    <div>
      <h2>你的项目</h2>
      <ul>
        {optimisticItems.map(item => (
          <li
            key={item.id}
            style={{
              opacity: item.deleting ? 0.5 : 1,
              textDecoration: item.deleting ? 'line-through' : 'none',
              transition: 'opacity 0.2s'
            }}
          >
            {item.name}
            <button
              onClick={() => handleDelete(item.id)}
              disabled={item.deleting}
              style={{ marginLeft: 8 }}
            >
              {item.deleting ? '删除中...' : '删除'}
            </button>
          </li>
        ))}
      </ul>
      {error && (
        <p style={{ color: 'red', padding: 8, background: '#fee' }}>
          {error}
        </p>
      )}
    </div>
  );
}
js src/actions.js hidden 复制代码
export async function deleteItem(id) {
  await new Promise((res) => setTimeout(res, 1000));
  // 项目 3 总是失败以演示错误恢复
  if (id === 3) {
    throw new Error('无法删除。权限不足。');
  }
}

尝试删除“部署到生产环境”。当删除失败时,该项目会自动重新出现在列表中。


故障排除 {/troubleshooting/}

我遇到了错误:“在 Transition 或 Action 之外发生了乐观状态更新” {/an-optimistic-state-update-occurred-outside-a-transition-or-action/}

你可能会看到此错误:

An optimistic state update occurred outside a Transition or Action. To fix, move the update to an Action, or wrap with startTransition.

乐观 setter 函数必须在 startTransition 内部调用:

js 复制代码
// 🚩 错误:在 Transition 之外
function handleClick() {
  setOptimistic(newValue);  // 警告!
  // ...
}

// ✅ 正确:在 Transition 内部
function handleClick() {
  startTransition(async () => {
    setOptimistic(newValue);
    // ...
  });
}

// ✅ 也正确:在 Action prop 内部
function submitAction(formData) {
  setOptimistic(newValue);
  // ...
}

当你在 Action 之外调用 setter 时,乐观状态会短暂出现,然后立即恢复为原始值。这是因为没有 Transition 来在 Action 运行时“保持”乐观状态。

我遇到了错误:“渲染时无法更新乐观状态” {/cannot-update-optimistic-state-while-rendering/}

你可能会看到此错误:

Cannot update optimistic state while rendering.

当你在组件渲染阶段调用乐观 setter 时,会发生此错误。你只能在事件处理器、effects 或其他回调中调用它:

js 复制代码
// 🚩 错误:渲染期间调用
function MyComponent({ items }) {
  const [isPending, setPending] = useOptimistic(false);

  // 这会在渲染期间运行——不允许!
  setPending(true);

  // ...
}

// ✅ 正确:在 startTransition 内部调用
function MyComponent({ items }) {
  const [isPending, setPending] = useOptimistic(false);

  function handleClick() {
    startTransition(() => {
      setPending(true);
      // ...
    });
  }

  // ...
}

// ✅ 也正确:从 Action 调用
function MyComponent({ items }) {
  const [isPending, setPending] = useOptimistic(false);

  function action() {
    setPending(true);
    // ...
  }

  // ...
}

我的乐观更新显示过期值 {/my-optimistic-updates-show-stale-values/}

如果你的乐观状态似乎基于旧数据,请考虑使用更新函数或 reducer 来相对于当前状态计算乐观状态。

js 复制代码
// 如果状态在 Action 期间发生变化,可能会显示过期数据
const [optimistic, setOptimistic] = useOptimistic(count);
setOptimistic(5);  // 总是设置为 5,即使 count 已改变

// 更好:相对更新能正确处理状态变化
const [optimistic, adjust] = useOptimistic(count, (current, delta) => current + delta);
adjust(1);  // 总是将当前 count 加 1

参见 基于当前状态更新状态 了解详情。

我不知道我的乐观更新是否在等待 {/i-dont-know-if-my-optimistic-update-is-pending/}

要了解 useOptimistic 是否处于待处理状态,你有三个选项:

  1. 检查 optimisticValue === value
js 复制代码
const [optimistic, setOptimistic] = useOptimistic(value);
const isPending = optimistic !== value;

如果值不相等,则说明有 Transition 正在进行。

  1. 添加 useTransition
js 复制代码
const [isPending, startTransition] = useTransition();
const [optimistic, setOptimistic] = useOptimistic(value);

//...
startTransition(() => {
  setOptimistic(state);
})

由于 useTransition 在底层使用 useOptimistic 来计算 isPending,这与选项 1 等价。

  1. 在 reducer 中添加 pending 标记
js 复制代码
const [optimistic, addOptimistic] = useOptimistic(
  items,
  (state, newItem) => [...state, { ...newItem, isPending: true }]
);

由于每个乐观项都有自己的标记,你可以为单个项显示加载状态。

帮助我们改进文档

发现翻译问题或内容错误?请告诉我们。