useActionState

useActionState 是一个 React Hook,它允许你使用 Actions 通过副作用来更新状态。

const [state, dispatchAction, isPending] = useActionState(reducerAction, initialState, permalink?);

参考

🌐 Reference

在组件的顶层调用 useActionState 来为一个操作的结果创建状态。

🌐 Call useActionState at the top level of your component to create state for the result of an Action.

import { useActionState } from 'react';

function reducerAction(previousState, actionPayload) {
// ...
}

function MyCart({initialState}) {
const [state, dispatchAction, isPending] = useActionState(reducerAction, initialState);
// ...
}

查看更多示例。

参数

🌐 Parameters

  • reducerAction:在触发动作时要调用的函数。调用时,它接收先前的状态(最初是你提供的 initialState,然后是其先前的返回值)作为第一个参数,随后是传递给 dispatchActionactionPayload
  • initialState:你希望状态最初的值。在首次调用 dispatchAction 后,React 会忽略此参数。
  • 可选 permalink:一个包含此表单修改的唯一页面 URL 的字符串。
    • 用于具有渐进增强功能的 React 服务器组件 页面上。
    • 如果 reducerAction 是一个 服务器功能 并且在 JavaScript 包加载之前提交表单,浏览器将跳转到指定的永久链接 URL,而不是当前页面的 URL。

返回

🌐 Returns

useActionState 返回一个包含恰好三个值的数组:

  1. 当前状态。在首次渲染期间,它将匹配你传入的 initialState。在调用 dispatchAction 之后,它将匹配 reducerAction 返回的值。
  2. 一个 dispatchAction 函数,你可以在 Actions 中调用它。
  3. isPending 标志,告诉你此 Hook 是否有任何已派发的动作正在等待中。

注意事项

🌐 Caveats

  • useActionState 是一个 Hook,因此你只能在你的组件的顶层或你自己的 Hook 中调用它。你不能在循环或条件语句中调用它。如果你需要那样做,可以提取一个新组件并将状态移入其中。
  • React 会将多个对 dispatchAction 的调用排队并按顺序执行。每次对 reducerAction 的调用都会接收上一次调用的结果。
  • dispatchAction 函数具有稳定的标识,因此你经常会看到它在 Effect 依赖中被省略,但包括它不会导致 Effect 被触发。如果 linter 允许你在没有错误的情况下省略依赖,这是安全的。 了解更多关于移除 Effect 依赖的信息。
  • 在使用 permalink 选项时,确保在目标页面上呈现相同的表单组件(包括相同的 reducerActionpermalink),以便 React 知道如何传递状态。一旦页面变得可交互,该参数将不再起作用。
  • 在使用服务器函数时,initialState 需要是可序列化的(例如普通对象、数组、字符串和数字)。
  • 如果 dispatchAction 抛出错误,React 会取消所有排队的操作并显示最近的 错误边界
  • 如果有多个正在进行的操作,React 会将它们批量处理。这是一个可能在未来版本中被移除的限制。

注意

dispatchAction 必须从一个动作中调用。

你可以将它封装在 startTransition 中,或将其传递给 Action prop。在该范围之外的调用不会被视为 Transition 的一部分,并且在开发模式下会 记录错误

🌐 You can wrap it in startTransition, or pass it to an Action prop. Calls outside that scope won’t be treated as part of the Transition and log an error on development mode.


reducerAction 函数

🌐 reducerAction function

传递给 useActionStatereducerAction 函数接收先前的状态并返回一个新状态。

🌐 The reducerAction function passed to useActionState receives the previous state and returns a new state.

useReducer 中的 reducers 不同,reducerAction 可以是异步的并执行副作用:

🌐 Unlike reducers in useReducer, the reducerAction can be async and perform side effects:

async function reducerAction(previousState, actionPayload) {
const newState = await post(actionPayload);
return newState;
}

每次你调用 dispatchAction 时,React 都会使用 actionPayload 调用 reducerAction。reducer 将执行副作用,例如提交数据,并返回新的状态。如果多次调用 dispatchAction,React 会按顺序排队并执行它们,因此上一次调用的结果会作为当前调用的 previousState 传递。

🌐 Each time you call dispatchAction, React calls the reducerAction with the actionPayload. The reducer will perform side effects such as posting data, and return the new state. If dispatchAction is called multiple times, React queues and executes them in order so the result of the previous call is passed as previousState for the current call.

参数

🌐 Parameters

  • previousState:最后的状态。最初,它等于 initialState。在第一次调用 dispatchAction 之后,它等于返回的最后状态。
  • 可选 actionPayload:传递给 dispatchAction 的参数。它可以是任何类型的值。类似于 useReducer 约定,它通常是一个带有 type 属性以标识它的对象,并且可选地包含其他附加信息的属性。

返回

🌐 Returns

reducerAction 返回新的状态,并触发一个过渡以使用该状态重新渲染。

注意事项

🌐 Caveats

  • reducerAction 可以是同步或异步的。它可以执行同步操作,例如显示通知,或者异步操作,例如向服务器发布更新。
  • reducerAction<StrictMode> 中不会被调用两次,因为 reducerAction 的设计允许副作用。
  • reducerAction 的返回类型必须与 initialState 的类型匹配。如果 TypeScript 推断出不匹配,你可能需要显式标注你的状态类型。
  • 如果你在 reducerAction 中的 await 之后设置状态,你目前需要将状态更新封装在额外的 startTransition 中。更多信息请参阅 startTransition 文档。
  • 在使用服务器函数时,actionPayload 需要是可序列化的(例如普通对象、数组、字符串和数字)。
深入研究

为什么它被称为 reducerAction

🌐 Why is it called reducerAction?

传递给 useActionState 的函数被称为reducer action,因为:

🌐 The function passed to useActionState is called a reducer action because:

  • 先前的状态简化为一个新的状态,就像useReducer一样。
  • 它是一个动作,因为它在转换中被调用,并且可以执行副作用。

从概念上讲,useActionState 类似于 useReducer,但你可以在 reducer 中执行副作用。

🌐 Conceptually, useActionState is like useReducer, but you can do side effects in the reducer.


用法

🌐 Usage

向动作添加状态

🌐 Adding state to an Action

在组件的顶层调用 useActionState 来为一个操作的结果创建状态。

🌐 Call useActionState at the top level of your component to create state for the result of an Action.

import { useActionState } from 'react';

async function addToCartAction(prevCount) {
// ...
}
function Counter() {
const [count, dispatchAction, isPending] = useActionState(addToCartAction, 0);

// ...
}

useActionState 返回一个恰好包含三项的数组:

  1. 当前状态,最初设置为你提供的初始状态。
  2. 允许你触发 reducerAction动作分发器
  3. A 待处理状态 ,告诉你该操作是否正在进行中。

要调用 addToCartAction,请调用 动作分发器。React 会将对 addToCartAction 的调用与之前的计数排队。

import { useActionState, startTransition } from 'react';
import { addToCart } from './api';
import Total from './Total';

export default function Checkout() {
  const [count, dispatchAction, isPending] = useActionState(async (prevCount) => {
    return await addToCart(prevCount)
  }, 0);

  function handleClick() {
    startTransition(() => {
      dispatchAction();
    });
  }

  return (
    <div className="checkout">
      <h2>Checkout</h2>
      <div className="row">
        <span>Eras Tour Tickets</span>
        <span>Qty: {count}</span>
      </div>
      <div className="row">
        <button onClick={handleClick}>Add Ticket{isPending ? ' 🌀' : '  '}</button>
      </div>
      <hr />
      <Total quantity={count} />
    </div>
  );
}

每次你点击“添加票”,React 会将对 addToCartAction 的调用排入队列。React 会显示挂起状态,直到所有票都被添加,然后以最终状态重新渲染。

🌐 Every time you click “Add Ticket,” React queues a call to addToCartAction. React shows the pending state until all the tickets are added, and then re-renders with the final state.

深入研究

useActionState 排队是如何工作的

🌐 How useActionState queuing works

尝试多次点击“添加票”。每次点击时,都会排队一个新的 addToCartAction。由于有一个人为的1秒延迟,这意味着4次点击大约需要4秒才能完成。

🌐 Try clicking “Add Ticket” multiple times. Every time you click, a new addToCartAction is queued. Since there’s an artificial 1 second delay, that means 4 clicks will take ~4 seconds to complete.

这是 useActionState 设计上的刻意安排。

我们必须等待 addToCartAction 的上一个结果,以便将 prevCount 传递给下一次对 addToCartAction 的调用。这意味着 React 必须等待上一个 Action 完成之后才能调用下一个 Action。

🌐 We have to wait for the previous result of addToCartAction in order to pass the prevCount to the next call to addToCartAction. That means React has to wait for the previous Action to finish before calling the next Action.

通常你可以通过使用 useOptimistic 来解决这个问题,但对于更复杂的情况,你可能需要考虑取消排队的操作或不使用 useActionState

🌐 You can typically solve this by using with useOptimistic but for more complex cases you may want to consider cancelling queued actions or not using useActionState.


使用多种动作类型

🌐 Using multiple Action types

要处理多种类型,你可以向 dispatchAction 传递一个参数。

🌐 To handle multiple types, you can pass an argument to dispatchAction.

按照惯例,通常将其写成 switch 语句。在 switch 的每个 case 中,计算并返回某个下一个状态。参数可以有任意形状,但通常传递带有 type 属性用于标识动作的对象。

🌐 By convention, it is common to write it as a switch statement. For each case in the switch, calculate and return some next state. The argument can have any shape, but it is common to pass objects with a type property identifying the action.

import { useActionState, startTransition } from 'react';
import { addToCart, removeFromCart } from './api';
import Total from './Total';

export default function Checkout() {
  const [count, dispatchAction, isPending] = useActionState(updateCartAction, 0);

  function handleAdd() {
    startTransition(() => {
      dispatchAction({ type: 'ADD' });
    });
  }

  function handleRemove() {
    startTransition(() => {
      dispatchAction({ type: 'REMOVE' });
    });
  }

  return (
    <div className="checkout">
      <h2>Checkout</h2>
      <div className="row">
        <span>Eras Tour Tickets</span>
        <span className="stepper">
          <span className="qty">{isPending ? '🌀' : count}</span>
          <span className="buttons">
            <button onClick={handleAdd}></button>
            <button onClick={handleRemove}></button>
          </span>
        </span>
      </div>
      <hr />
      <Total quantity={count} isPending={isPending}/>
    </div>
  );
}

async function updateCartAction(prevCount, actionPayload) {
  switch (actionPayload.type) {
    case 'ADD': {
      return await addToCart(prevCount);
    }
    case 'REMOVE': {
      return await removeFromCart(prevCount);
    }
  }
  return prevCount;
}

当你点击增加或减少数量时,会触发一个 "ADD""REMOVE"。在 reducerAction 中,会调用不同的 API 来更新数量。

🌐 When you click to increase or decrease the quantity, an "ADD" or "REMOVE" is dispatched. In the reducerAction, different APIs are called to update the quantity.

在此示例中,我们使用操作的挂起状态来替换数量和总量。如果你想提供即时反馈,例如立即更新数量,可以使用 useOptimistic

🌐 In this example, we use the pending state of the Actions to replace both the quantity and the total. If you want to provide immediate feedback, such as immediately updating the quantity, you can use useOptimistic.

深入研究

useActionStateuseReducer 有什么不同?

🌐 How is useActionState different from useReducer?

你可能会注意到这个例子看起来很像 useReducer,但它们的用途不同:

🌐 You might notice this example looks a lot like useReducer, but they serve different purposes:

  • 使用 useReducer 来管理你的 UI 状态。reducer 必须是纯函数。
  • 使用 useActionState 来管理你的 Actions 状态。reducer 可以执行副作用。

你可以把 useActionState 看作是 useReducer,用于处理用户操作的副作用。由于它是基于上一个操作来计算下一步要执行的操作,因此必须按顺序调用。如果你想并行执行操作,请直接使用 useStateuseTransition

🌐 You can think of useActionState as useReducer for side effects from user Actions. Since it computes the next Action to take based on the previous Action, it has to order the calls sequentially. If you want to perform Actions in parallel, use useState and useTransition directly.


useOptimistic 一起使用

🌐 Using with useOptimistic

你可以将 useActionStateuseOptimistic 结合使用,以显示即时的 UI 反馈:

🌐 You can combine useActionState with useOptimistic to show immediate UI feedback:

import { useActionState, startTransition, useOptimistic } from 'react';
import { addToCart, removeFromCart } from './api';
import Total from './Total';

export default function Checkout() {
  const [count, dispatchAction, isPending] = useActionState(updateCartAction, 0);
  const [optimisticCount, setOptimisticCount] = useOptimistic(count);

  function handleAdd() {
    startTransition(() => {
      setOptimisticCount(c => c + 1);
      dispatchAction({ type: 'ADD' });
    });
  }

  function handleRemove() {
    startTransition(() => {
      setOptimisticCount(c => c - 1);
      dispatchAction({ type: 'REMOVE' });
    });
  }

  return (
    <div className="checkout">
      <h2>Checkout</h2>
      <div className="row">
        <span>Eras Tour Tickets</span>
        <span className="stepper">
          <span className="pending">{isPending && '🌀'}</span>
          <span className="qty">{optimisticCount}</span>
          <span className="buttons">
            <button onClick={handleAdd}></button>
            <button onClick={handleRemove}></button>
          </span>
        </span>
      </div>
      <hr />
      <Total quantity={optimisticCount} isPending={isPending}/>
    </div>
  );
}

async function updateCartAction(prevCount, actionPayload) {
  switch (actionPayload.type) {
    case 'ADD': {
      return await addToCart(prevCount);
    }
    case 'REMOVE': {
      return await removeFromCart(prevCount);
    }
  }
  return prevCount;
}

setOptimisticCount 会立即更新数量,而 dispatchAction() 会将 updateCartAction 加入队列。数量和总计上都会出现待处理指示器,以向用户反馈他们的更新仍在应用中。


与动作属性一起使用

🌐 Using with Action props

当你将 dispatchAction 函数传递给一个公开 Action prop 的组件时,你不需要自己调用 startTransitionuseOptimistic

🌐 When you pass the dispatchAction function to a component that exposes an Action prop, you don’t need to call startTransition or useOptimistic yourself.

此示例显示了如何使用 QuantityStepper 组件的 increaseActiondecreaseAction 属性:

🌐 This example shows using the increaseAction and decreaseAction props of a QuantityStepper component:

import { useActionState } from 'react';
import { addToCart, removeFromCart } from './api';
import QuantityStepper from './QuantityStepper';
import Total from './Total';

export default function Checkout() {
  const [count, dispatchAction, isPending] = useActionState(updateCartAction, 0);

  function addAction() {
    dispatchAction({type: 'ADD'});
  }

  function removeAction() {
    dispatchAction({type: 'REMOVE'});
  }

  return (
    <div className="checkout">
      <h2>Checkout</h2>
      <div className="row">
        <span>Eras Tour Tickets</span>
        <QuantityStepper
          value={count}
          increaseAction={addAction}
          decreaseAction={removeAction}
        />
      </div>
      <hr />
      <Total quantity={count} isPending={isPending} />
    </div>
  );
}

async function updateCartAction(prevCount, actionPayload) {
  switch (actionPayload.type) {
    case 'ADD': {
      return await addToCart(prevCount);
    }
    case 'REMOVE': {
      return await removeFromCart(prevCount);
    }
  }
  return prevCount;
}

由于 <QuantityStepper> 内置了对过渡、挂起状态和乐观更新计数的支持,你只需要告诉动作要改变什么,而如何改变将由它为你处理。

🌐 Since <QuantityStepper> has built-in support for transitions, pending state, and optimistically updating the count, you just need to tell the Action what to change, and how to change it is handled for you.


取消排队的操作

🌐 Cancelling queued Actions

你可以使用 AbortController 来取消待处理的操作:

🌐 You can use an AbortController to cancel pending Actions:

import { useActionState, useRef } from 'react';
import { addToCart, removeFromCart } from './api';
import QuantityStepper from './QuantityStepper';
import Total from './Total';

export default function Checkout() {
  const abortRef = useRef(null);
  const [count, dispatchAction, isPending] = useActionState(updateCartAction, 0);
  
  async function addAction() {
    if (abortRef.current) {
      abortRef.current.abort();
    }
    abortRef.current = new AbortController();
    await dispatchAction({ type: 'ADD', signal: abortRef.current.signal });
  }

  async function removeAction() {
    if (abortRef.current) {
      abortRef.current.abort();
    }
    abortRef.current = new AbortController();
    await dispatchAction({ type: 'REMOVE', signal: abortRef.current.signal });
  }

  return (
    <div className="checkout">
      <h2>Checkout</h2>
      <div className="row">
        <span>Eras Tour Tickets</span>
        <QuantityStepper
          value={count}
          increaseAction={addAction}
          decreaseAction={removeAction}
        />
      </div>
      <hr />
      <Total quantity={count} isPending={isPending} />
    </div>
  );
}

async function updateCartAction(prevCount, actionPayload) {
  switch (actionPayload.type) {
    case 'ADD': {
      try {
        return await addToCart(prevCount, { signal: actionPayload.signal });
      } catch (e) {
        return prevCount + 1;
      }
    }
    case 'REMOVE': {
      try {
        return await removeFromCart(prevCount, { signal: actionPayload.signal });
      } catch (e) {
        return Math.max(0, prevCount - 1);
      }
    }
  }
  return prevCount;
}

尝试多次点击增加或减少,并注意总数会在 1 秒内更新,无论你点击多少次。这之所以有效,是因为它使用了一个 AbortController 来“完成”前一个操作,以便下一个操作可以继续。

🌐 Try clicking increase or decrease multiple times, and notice that the total updates within 1 second no matter how many times you click. This works because it uses an AbortController to “complete” the previous Action so the next Action can proceed.

易犯错误

中止操作并不总是安全的。

🌐 Aborting an Action isn’t always safe.

例如,如果操作执行了修改(比如写入数据库),中止网络请求并不会撤销服务器端的更改。这就是为什么 useActionState 默认不会中止。只有在你知道副作用可以安全忽略或重试时,才是安全的。

🌐 For example, if the Action performs a mutation (like writing to a database), aborting the network request doesn’t undo the server-side change. This is why useActionState doesn’t abort by default. It’s only safe when you know the side effect can be safely ignored or retried.


<form> Action 属性一起使用

🌐 Using with <form> Action props

你可以将 dispatchAction 函数作为 <form>action 属性传递。

🌐 You can pass the dispatchAction function as the action prop to a <form>.

以这种方式使用时,React 会自动将提交封装在一个 Transition 中,因此你不需要自己调用 startTransitionreducerAction 会接收之前的状态和已提交的 FormData

🌐 When used this way, React automatically wraps the submission in a Transition, so you don’t need to call startTransition yourself. The reducerAction receives the previous state and the submitted FormData:

import { useActionState, useOptimistic } from 'react';
import { addToCart, removeFromCart } from './api';
import Total from './Total';

export default function Checkout() {
  const [count, dispatchAction, isPending] = useActionState(updateCartAction, 0);
  const [optimisticCount, setOptimisticCount] = useOptimistic(count);

  async function formAction(formData) {
    const type = formData.get('type');
    if (type === 'ADD') {
      setOptimisticCount(c => c + 1);
    } else {
      setOptimisticCount(c => Math.max(0, c - 1));
    }
    return dispatchAction(formData);
  }

  return (
    <form action={formAction} className="checkout">
      <h2>Checkout</h2>
      <div className="row">
        <span>Eras Tour Tickets</span>
        <span className="stepper">
          <span className="pending">{isPending && '🌀'}</span>
          <span className="qty">{optimisticCount}</span>
          <span className="buttons">
            <button type="submit" name="type" value="ADD"></button>
            <button type="submit" name="type" value="REMOVE"></button>
          </span>
        </span>
      </div>
      <hr />
      <Total quantity={count} isPending={isPending} />
    </form>
  );
}

async function updateCartAction(prevCount, formData) {
  const type = formData.get('type');
  switch (type) {
    case 'ADD': {
      return await addToCart(prevCount);
    }
    case 'REMOVE': {
      return await removeFromCart(prevCount);
    }
  }
  return prevCount;
}

在这个示例中,当用户点击步进箭头时,按钮会提交表单,并且 useActionState 使用表单数据调用 updateCartAction。该示例使用 useOptimistic 来在服务器确认更新的同时立即显示新的数量。

🌐 In this example, when the user clicks the stepper arrows, the button submits the form and useActionState calls updateCartAction with the form data. The example uses useOptimistic to immediately show the new quantity while the server confirms the update.

React Server Components

当与 Server Function 一起使用时,useActionState 允许在水合(React 附加到服务器渲染的 HTML)完成之前显示服务器的响应。你还可以使用可选的 permalink 参数来进行渐进增强(允许表单在 JavaScript 加载之前工作),适用于具有动态内容的页面。这通常由你的框架为你处理。

🌐 When used with a Server Function, useActionState allows the server’s response to be shown before hydration (when React attaches to server-rendered HTML) completes. You can also use the optional permalink parameter for progressive enhancement (allowing the form to work before JavaScript loads) on pages with dynamic content. This is typically handled by your framework for you.

有关在表单中使用 Actions 的更多信息,请参阅 <form> 文档。

🌐 See the <form> docs for more information on using Actions with forms.


处理错误

🌐 Handling errors

有两种方法可以处理 useActionState 的错误。

🌐 There are two ways to handle errors with useActionState.

对于已知错误,例如来自后端的“数量不可用”验证错误,你可以将其作为 reducerAction 状态的一部分返回,并在用户界面中显示。

🌐 For known errors, such as “quantity not available” validation errors from your backend, you can return it as part of your reducerAction state and display it in the UI.

对于未知错误,例如 undefined is not a function,你可以抛出一个错误。React 将取消所有排队的操作,并通过从 useActionState 钩子重新抛出错误来显示最近的 错误边界

🌐 For unknown errors, such as undefined is not a function, you can throw an error. React will cancel all queued Actions and shows the nearest Error Boundary by rethrowing the error from the useActionState hook.

import {useActionState, startTransition} from 'react';
import {ErrorBoundary} from 'react-error-boundary';
import {addToCart} from './api';
import Total from './Total';

function Checkout() {
  const [state, dispatchAction, isPending] = useActionState(
    async (prevState, quantity) => {
      const result = await addToCart(prevState.count, quantity);
      if (result.error) {
        // Return the error from the API as state
        return {...prevState, error: `Could not add quanitiy ${quantity}: ${result.error}`};
      }
      
      if (!isPending) {
        // Clear the error state for the first dispatch.
        return {count: result.count, error: null};    
      }
      
      // Return the new count, and any errors that happened.
      return {count: result.count, error: prevState.error};
      
      
    },
    {
      count: 0,
      error: null,
    }
  );

  function handleAdd(quantity) {
    startTransition(() => {
      dispatchAction(quantity);
    });
  }

  return (
    <div className="checkout">
      <h2>Checkout</h2>
      <div className="row">
        <span>Eras Tour Tickets</span>
        <span>
          {isPending && '🌀 '}Qty: {state.count}
        </span>
      </div>
      <div className="buttons">
        <button onClick={() => handleAdd(1)}>Add 1</button>
        <button onClick={() => handleAdd(10)}>Add 10</button>
        <button onClick={() => handleAdd(NaN)}>Add NaN</button>
      </div>
      {state.error && <div className="error">{state.error}</div>}
      <hr />
      <Total quantity={state.count} isPending={isPending} />
    </div>
  );
}



export default function App() {
  return (
    <ErrorBoundary
      fallbackRender={({resetErrorBoundary}) => (
        <div className="checkout">
          <h2>Something went wrong</h2>
          <p>The action could not be completed.</p>
          <button onClick={resetErrorBoundary}>Try again</button>
        </div>
      )}>
      <Checkout />
    </ErrorBoundary>
  );
}

在这个示例中,“Add 10”模拟了一个返回验证错误的 API,updateCartAction 将其存储在状态中并内联显示。“Add NaN”会导致计数无效,因此 updateCartAction 会抛出错误,该错误通过 useActionState 传播到 ErrorBoundary 并显示重置的用户界面。

🌐 In this example, “Add 10” simulates an API that returns a validation error, which updateCartAction stores in state and displays inline. “Add NaN” results in an invalid count, so updateCartAction throws, which propagates through useActionState to the ErrorBoundary and shows a reset UI.


故障排除

🌐 Troubleshooting

我的 isPending 标志没有更新

🌐 My isPending flag is not updating

如果你手动调用 dispatchAction(而不是通过 Action 属性),请确保将调用封装在 startTransition 中:

🌐 If you’re calling dispatchAction manually (not through an Action prop), make sure you wrap the call in startTransition:

import { useActionState, startTransition } from 'react';

function MyComponent() {
const [state, dispatchAction, isPending] = useActionState(myAction, null);

function handleClick() {
// ✅ Correct: wrap in startTransition
startTransition(() => {
dispatchAction();
});
}

// ...
}

当将 dispatchAction 传递给 Action 属性时,React 会自动将其封装在一个 Transition 中。

🌐 When dispatchAction is passed to an Action prop, React automatically wraps it in a Transition.


我的操作无法读取表单数据

🌐 My Action cannot read form data

当你使用 useActionState 时,reducerAction 会将一个额外的参数作为它的第一个参数:前一个或初始的状态。因此,提交的表单数据是它的第二个参数,而不是第一个参数。

🌐 When you use useActionState, the reducerAction receives an extra argument as its first argument: the previous or initial state. The submitted form data is therefore its second argument instead of its first.

// Without useActionState
function action(formData) {
const name = formData.get('name');
}

// With useActionState
function action(prevState, formData) {
const name = formData.get('name');
}

我的动作正在被跳过

🌐 My actions are being skipped

如果你多次调用 dispatchAction,而其中一些没有运行,这可能是因为之前的 dispatchAction 调用抛出了错误。

🌐 If you call dispatchAction multiple times and some of them don’t run, it may be because an earlier dispatchAction call threw an error.

reducerAction 抛出时,React 会跳过所有随后排队的 dispatchAction 调用。

🌐 When a reducerAction throws, React skips all subsequently queued dispatchAction calls.

为处理此问题,请在你的 reducerAction 中捕获错误,并返回错误状态,而不是抛出异常:

🌐 To handle this, catch errors within your reducerAction and return an error state instead of throwing:

async function myReducerAction(prevState, data) {
try {
const result = await submitData(data);
return { success: true, data: result };
} catch (error) {
// ✅ Return error state instead of throwing
return { success: false, error: error.message };
}
}

我的状态没有重置

🌐 My state doesn’t reset

useActionState 没有提供内置的重置功能。要重置状态,你可以设计你的 reducerAction 来处理重置信号:

const initialState = { name: '', error: null };

async function formAction(prevState, payload) {
// Handle reset
if (payload === null) {
return initialState;
}
// Normal action logic
const result = await submitData(payload);
return result;
}

function MyComponent() {
const [state, dispatchAction, isPending] = useActionState(formAction, initialState);

function handleReset() {
startTransition(() => {
dispatchAction(null); // Pass null to trigger reset
});
}

// ...
}

或者,你可以使用 useActionState 向组件添加一个 key 属性,以强制其以新的状态重新挂载,或者使用 <form> action 属性,该属性在提交后会自动重置。

🌐 Alternatively, you can add a key prop to the component using useActionState to force it to remount with fresh state, or a <form> action prop, which resets automatically after submission.


我收到一个错误:“在过渡之外调用了使用 useActionState 的异步函数。”

🌐 I’m getting an error: “An async function with useActionState was called outside of a transition.”

一个常见的错误是在 Transition 内部忘记调用 dispatchAction

🌐 A common mistake is to forget to call dispatchAction from inside a Transition:

Console
An async function with useActionState was called outside of a transition. This is likely not what you intended (for example, isPending will not update correctly). Either call the returned function inside startTransition, or pass it to an action or formAction prop.

此错误发生是因为 dispatchAction 必须在过渡中运行:

🌐 This error happens because dispatchAction must run inside a Transition:

function MyComponent() {
const [state, dispatchAction, isPending] = useActionState(myAsyncAction, null);

function handleClick() {
// ❌ Wrong: calling dispatchAction outside a Transition
dispatchAction();
}

// ...
}

要修复,请将调用封装在 startTransition 中:

🌐 To fix, either wrap the call in startTransition:

import { useActionState, startTransition } from 'react';

function MyComponent() {
const [state, dispatchAction, isPending] = useActionState(myAsyncAction, null);

function handleClick() {
// ✅ Correct: wrap in startTransition
startTransition(() => {
dispatchAction();
});
}

// ...
}

或者将 dispatchAction 传递给 Action 属性,在 Transition 中调用:

🌐 Or pass dispatchAction to an Action prop, is call in a Transition:

function MyComponent() {
const [state, dispatchAction, isPending] = useActionState(myAsyncAction, null);

// ✅ Correct: action prop wraps in a Transition for you
return <Button action={dispatchAction}>...</Button>;
}

我收到一个错误:“在渲染时无法更新操作状态”

🌐 I’m getting an error: “Cannot update action state while rendering”

你不能在渲染期间调用 dispatchAction

🌐 You cannot call dispatchAction during render:

Console

这会导致无限循环,因为调用 dispatchAction 会安排状态更新,这会触发重新渲染,而重新渲染又会再次调用 dispatchAction

🌐 This causes an infinite loop because calling dispatchAction schedules a state update, which triggers a re-render, which calls dispatchAction again.

function MyComponent() {
const [state, dispatchAction, isPending] = useActionState(myAction, null);

// ❌ Wrong: calling dispatchAction during render
dispatchAction();

// ...
}

要修复,只在响应用户事件(如表单提交或按钮点击)时调用 dispatchAction

🌐 To fix, only call dispatchAction in response to user events (like form submissions or button clicks).