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

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);
// ...
}

Veja mais exemplos abaixo.

Parâmetros

  • reducerAction: A função a ser chamada quando a Ação é acionada. Quando chamada, ela recebe o estado anterior (inicialmente o initialState que você forneceu, depois seu valor de retorno anterior) como seu primeiro argumento, seguido pelo actionPayload passado para dispatchAction.
  • initialState: O valor que você deseja que o estado seja inicialmente. O React ignora este argumento após dispatchAction ser 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:

  1. O estado atual. Durante a primeira renderização, ele corresponderá ao initialState que você passou. Depois que dispatchAction for invocado, ele corresponderá ao valor retornado pela reducerAction.
  2. Uma função dispatchAction que você chama dentro de Ações.
  3. A flag isPending que 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 dispatchAction sequencialmente. Cada chamada para reducerAction recebe o resultado da chamada anterior.
  • A função dispatchAction tem 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 mesma reducerAction e permalink) 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, initialState precisa ser serializável (valores como objetos simples, arrays, strings e números).
  • Se dispatchAction lanç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.

Note

dispatchAction deve ser chamado de dentro de uma Ação.

Você pode envolvê-lo em startTransition, ou passá-lo para uma prop de Ação. Chamadas fora desse escopo não serão tratadas como parte da Transition e registrarão um erro no modo de desenvolvimento.


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 ao initialState. Após a primeira chamada para dispatchAction, é igual ao último estado retornado.

  • opcional actionPayload: O argumento passado para dispatchAction. Pode ser um valor de qualquer tipo. Seguindo as convenções de useReducer, geralmente é um objeto com uma propriedade type que 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

  • reducerAction pode 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.
  • reducerAction não é invocada duas vezes no <StrictMode> pois reducerAction foi projetada para permitir efeitos colaterais.
  • O tipo de retorno de reducerAction deve corresponder ao tipo de initialState. Se o TypeScript inferir uma incompatibilidade, pode ser necessário anotar explicitamente o tipo de seu estado.
  • Se você definir estado após await na reducerAction, atualmente você precisa envolver a atualização de estado em um startTransition adicional. Veja a documentação de startTransition para mais informações.
  • Ao usar Funções de Servidor, actionPayload precisa ser serializável (valores como objetos simples, arrays, strings e números).
Deep Dive

Por que se chama reducerAction?

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:

  1. O estado atual, inicialmente definido como o estado inicial que você forneceu.
  2. O dispatcher de ação que permite acionar a reducerAction.
  3. 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

Como funciona o enfileiramento do useActionState

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

How is useActionState different from useReducer?

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

  • Use useReducer to manage state of your UI. The reducer must be pure.

  • Use useActionState to 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.

Pitfall

Aborting an Action isn’t always safe.

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.


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.

React Server Components

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.

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:

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.

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:

Console
Cannot update action state while rendering.

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).