useActionState
useActionState é um Hook do React que permite que você atualize o estado com efeitos colaterais usando Ações.
const [state, dispatchAction, isPending] = useActionState(reducerAction, initialState, permalink?);- Referência
- Uso
- Solução de problemas
- Minha flag
isPendingnão está sendo atualizada - Minha Ação não consegue ler os dados do formulário enviado
- Minhas ações estão sendo ignoradas
- Meu estado não é resetado
- Estou recebendo um erro: “An async function with useActionState was called outside of a transition.”
- Estou recebendo um erro: “Cannot update action state while rendering”
- Minha flag
Referência
useActionState(reducerAction, initialState, permalink?)
Chame useActionState no nível superior do seu componente para criar um estado para o resultado de uma Ação.
import { useActionState } from 'react';
function reducerAction(previousState, actionPayload) {
// ...
}
function MyCart({initialState}) {
const [state, dispatchAction, isPending] = useActionState(reducerAction, initialState);
// ...
}Parâmetros
reducerAction: A função a ser chamada quando a Ação é acionada. Quando chamada, ela recebe o estado anterior (inicialmente oinitialStateque você forneceu, depois seu valor de retorno anterior) como seu primeiro argumento, seguido peloactionPayloadpassado paradispatchAction.initialState: O valor que você deseja que o estado seja inicialmente. O React ignora este argumento apósdispatchActionser invocado pela primeira vez.- opcional
permalink: Uma string contendo a URL exclusiva da página que este formulário modifica.- Para uso em páginas com Componentes de Servidor React com aprimoramento progressivo.
- Se
reducerActioné uma Função de Servidor e o formulário é enviado antes do carregamento do bundle JavaScript, o navegador navegará para a URL de permalink especificada, em vez da URL da página atual.
Retornos
useActionState retorna um array com exatamente três valores:
- O estado atual. Durante a primeira renderização, ele corresponderá ao
initialStateque você passou. Depois quedispatchActionfor invocado, ele corresponderá ao valor retornado pelareducerAction. - Uma função
dispatchActionque você chama dentro de Ações. - A flag
isPendingque informa se alguma Ação despachada para este Hook está pendente.
Ressalvas
useActionStateé um Hook, portanto você só pode chamá-lo no nível superior do seu componente ou de seus próprios Hooks. Você não pode chamá-lo dentro de loops ou condições. Se precisar disso, extraia um novo componente e mova o estado para ele.- O React enfileira e executa múltiplas chamadas para
dispatchActionsequencialmente. Cada chamada parareducerActionrecebe o resultado da chamada anterior. - A função
dispatchActiontem uma identidade estável, então você frequentemente a verá omitida das dependências de Efeitos, mas incluí-la não fará o Efeito disparar. Se o linter permitir omitir uma dependência sem erros, é seguro fazê-lo. Saiba mais sobre remoção de dependências de Efeitos. - Ao usar a opção
permalink, garanta que o mesmo componente de formulário seja renderizado na página de destino (incluindo a mesmareducerActionepermalink) para que o React saiba como passar o estado. Assim que a página se tornar interativa, este parâmetro não tem efeito. - Ao usar Funções de Servidor,
initialStateprecisa ser serializável (valores como objetos simples, arrays, strings e números). - Se
dispatchActionlançar um erro, o React cancela todas as ações enfileiradas e mostra o Error Boundary mais próximo. - Se houver múltiplas Ações em andamento, o React as agrupa. Esta é uma limitação que pode ser removida em uma versão futura.
Função reducerAction
A função reducerAction passada para useActionState recebe o estado anterior e retorna um novo estado.
Ao contrário dos reducers em useReducer, a reducerAction pode ser assíncrona e realizar efeitos colaterais:
async function reducerAction(previousState, actionPayload) {
const newState = await post(actionPayload);
return newState;
}Cada vez que você chama dispatchAction, o React chama a reducerAction com o actionPayload. O reducer realizará efeitos colaterais como postar dados e retornará o novo estado. Se dispatchAction for chamado várias vezes, o React enfileira e executa-os em ordem, para que o resultado da chamada anterior seja passado como previousState para a chamada atual.
Parâmetros
-
previousState: O último estado. Inicialmente, é igual aoinitialState. Após a primeira chamada paradispatchAction, é igual ao último estado retornado. -
opcional
actionPayload: O argumento passado paradispatchAction. Pode ser um valor de qualquer tipo. Seguindo as convenções deuseReducer, geralmente é um objeto com uma propriedadetypeque o identifica e, opcionalmente, outras propriedades com informações adicionais.
Retorna
reducerAction retorna o novo estado e aciona uma Transition para re-renderizar com esse estado.
Ressalvas
reducerActionpode ser síncrona ou assíncrona. Pode realizar ações síncronas como mostrar uma notificação, ou ações assíncronas como postar atualizações em um servidor.reducerActionnão é invocada duas vezes no<StrictMode>poisreducerActionfoi projetada para permitir efeitos colaterais.- O tipo de retorno de
reducerActiondeve corresponder ao tipo deinitialState. Se o TypeScript inferir uma incompatibilidade, pode ser necessário anotar explicitamente o tipo de seu estado. - Se você definir estado após
awaitnareducerAction, atualmente você precisa envolver a atualização de estado em umstartTransitionadicional. Veja a documentação de startTransition para mais informações. - Ao usar Funções de Servidor,
actionPayloadprecisa ser serializável (valores como objetos simples, arrays, strings e números).
Deep Dive
A função passada para useActionState é chamada de reducer action (ação redutora) porque:
- Ela reduz o estado anterior em um novo estado, como
useReducer. - É uma Ação porque é chamada dentro de uma Transition e pode realizar efeitos colaterais.
Conceitualmente, useActionState é como useReducer, mas você pode fazer efeitos colaterais no reducer.
Uso
Adicionando estado a uma Ação
Chame useActionState no nível superior do seu componente para criar um estado para o resultado de uma Ação.
import { useActionState } from 'react';
async function addToCartAction(prevCount) {
// ...
}
function Counter() {
const [count, dispatchAction, isPending] = useActionState(addToCartAction, 0);
// ...
}useActionState retorna um array com exatamente três itens:
- O estado atual, inicialmente definido como o estado inicial que você forneceu.
- O dispatcher de ação que permite acionar a
reducerAction. - Um estado pendente que informa se a Ação está em progresso.
Para chamar addToCartAction, chame o dispatcher de ação. O React enfileirará chamadas para addToCartAction com o count anterior.
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> ); }
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.
Deep Dive
Tente clicar em “Add Ticket” várias vezes. Cada vez que você clica, um novo addToCartAction é enfileirado. Como há um atraso artificial de 1 segundo, isso significa que 4 cliques levarão ~4 segundos para serem concluídos.
Isso é intencional no design do useActionState.
Temos que aguardar o resultado anterior de addToCartAction para passar o prevCount para a próxima chamada de addToCartAction. Isso significa que o React precisa esperar que a Ação anterior termine antes de chamar a próxima Ação.
Você normalmente pode resolver isso usando com useOptimistic, mas para casos mais complexos você pode considerar cancelar ações enfileiradas ou não usar useActionState.
Usando múltiplos tipos de Ação
Para lidar com múltiplos tipos, você pode passar um argumento para dispatchAction.
Por convenção, é comum escrevê-lo como uma instrução switch. Para cada caso no switch, calcule e retorne algum próximo estado. O argumento pode ter qualquer forma, mas é comum passar objetos com uma propriedade type identificando a ação.
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; }
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.
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.
Deep Dive
You might notice this example looks a lot like useReducer, but they serve different purposes:
-
Use
useReducerto manage state of your UI. The reducer must be pure. -
Use
useActionStateto manage state of your Actions. The reducer can perform side effects.
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.
Using with useOptimistic
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 immediately updates the quantity, and dispatchAction() queues the updateCartAction. A pending indicator appears on both the quantity and total to give the user feedback that their update is still being applied.
Using with Action props
When you pass the dispatchAction function to a component that exposes an Action prop, you don’t need to call startTransition or useOptimistic yourself.
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; }
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
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; }
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.
Using with <form> Action props
You can pass the dispatchAction function as the action prop to a <form>.
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; }
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.
See the <form> docs for more information on using Actions with forms.
Handling errors
There are two ways to handle errors with useActionState.
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.
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> ); }
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.
Solução de problemas
Minha flag isPending não está sendo atualizada
Se você estiver chamando dispatchAction manualmente (não por meio de uma prop de Ação), certifique-se de envolver a chamada em startTransition:
import { useActionState, startTransition } from 'react';
function MyComponent() {
const [state, dispatchAction, isPending] = useActionState(myAction, null);
function handleClick() {
// ✅ Correct: wrap in startTransition
startTransition(() => {
dispatchAction();
});
}
// ...
}Quando dispatchAction é passado para uma prop de Ação, o React automaticamente o envolve em uma Transition.
Minha Ação não consegue ler os dados do formulário enviado
Quando você usa useActionState, a reducerAction recebe um argumento extra como seu primeiro argumento: o estado anterior ou inicial. Os dados do formulário enviado são, portanto, seu segundo argumento em vez de seu primeiro.
// Sem useActionState
function action(formData) {
const name = formData.get('name');
}
// Com useActionState
function action(prevState, formData) {
const name = formData.get('name');
}Minhas ações estão sendo ignoradas
Se você chamar dispatchAction várias vezes e algumas delas não executarem, pode ser porque uma chamada anterior de dispatchAction lançou um erro.
Quando uma reducerAction lança um erro, o React ignora todas as chamadas de dispatchAction subsequentemente enfileiradas.
Para lidar com isso, capture erros dentro da sua reducerAction e retorne um estado de erro em vez de lançar:
async function myReducerAction(prevState, data) {
try {
const result = await submitData(data);
return { success: true, data: result };
} catch (error) {
// ✅ Retorne o estado de erro em vez de lançar
return { success: false, error: error.message };
}
}Meu estado não é resetado
useActionState não fornece uma função de reset embutida. Para resetar o estado, você pode projetar sua reducerAction para lidar com um sinal de reset:
const initialState = { name: '', error: null };
async function formAction(prevState, payload) {
// Lidar com reset
if (payload === null) {
return initialState;
}
// Lógica normal de ação
const result = await submitData(payload);
return result;
}
function MyComponent() {
const [state, dispatchAction, isPending] = useActionState(formAction, initialState);
function handleReset() {
startTransition(() => {
dispatchAction(null); // Passe null para acionar o reset
});
}
// ...
}Alternativamente, você pode adicionar uma prop key ao componente que usa useActionState para forçá-lo a remontar com estado novo, ou uma prop action do <form>, que é resetada automaticamente após o envio.
Estou recebendo um erro: “An async function with useActionState was called outside of a transition.”
Um erro comum é esquecer de chamar dispatchAction de dentro de uma Transition:
action or formAction prop.Este erro ocorre porque dispatchAction deve ser executado dentro de uma Transition:
function MyComponent() {
const [state, dispatchAction, isPending] = useActionState(myAsyncAction, null);
function handleClick() {
// ❌ Errado: chamando dispatchAction fora de uma Transition
dispatchAction();
}
// ...
}Para corrigir, envolva a chamada em startTransition:
import { useActionState, startTransition } from 'react';
function MyComponent() {
const [state, dispatchAction, isPending] = useActionState(myAsyncAction, null);
function handleClick() {
// ✅ Correto: envolva em startTransition
startTransition(() => {
dispatchAction();
});
}
// ...
}Ou passe dispatchAction para uma prop de Ação, que é chamada em uma Transition:
function MyComponent() {
const [state, dispatchAction, isPending] = useActionState(myAsyncAction, null);
// ✅ Correto: a prop action envolve em uma Transition para você
return <Button action={dispatchAction}>...</Button>;
}Estou recebendo um erro: “Cannot update action state while rendering”
Você não pode chamar dispatchAction durante a renderização:
Isso causa um loop infinito porque chamar dispatchAction agenda uma atualização de estado, que aciona uma re-renderização, que chama dispatchAction novamente.
function MyComponent() {
const [state, dispatchAction, isPending] = useActionState(myAction, null);
// ❌ Errado: chamando dispatchAction durante a renderização
dispatchAction();
// ...
}Para corrigir, chame dispatchAction apenas em resposta a eventos do usuário (como envios de formulário ou cliques de botão).