O que é o hook useActionState e como ele funciona?

O React 19 veio com dois novos recursos notáveis chamados server components e server actions.  A partir dessa versão, os componentes de servidor se tornaram o padrão em frameworks como Next.js que os suportam prontamente. As ações do servidor, por outro lado, são funções que são executadas no servidor para permitir o manuseio de formulários diretamente no servidor sem a necessidade de endpoints de API. Uma ação de servidor se parece com isto:
"use server";

async function submitForm(formData) {
  const name = formData.get("name");
  return { message: Hello, ${name}! };
}
Esta ação do servidor extrai um campo name de um formulário e retorna uma string saudando esse nome. Para simplificar o gerenciamento de estado para ações do servidor e eliminar a necessidade de JavaScript no lado do cliente para formulários simples, a equipe do React introduziu o hook useActionState na versão 19. Vamos dar uma olhada mais de perto neste hook e ver como ele funciona. A documentação do React descreve o hook useActionState como um hook que "permite atualizar o estado com base no resultado de uma ação de formulário." Mas isso não significa que você pode usar o hook useActionState apenas com formulários. Você também pode usá-lo para gerenciar cliques em botões e outros eventos, desde que você tenha uma ação configurada. E lembre-se de que, como useActionState é um hook, você não pode usá-lo dentro de um componente de servidor. Aqui está a sintaxe básica do hook useActionState:
const [state, action, isPending] = useActionState(actionFunction, initialState, permalink);
  • state é o estado atual que a ação retorna.
  • action é a função que dispara a ação do servidor.
  • isPending é um booleano que indica se a ação está sendo executada no momento ou não.
  • O parâmetro actionFunction é a própria ação do servidor.
  • initialState é o parâmetro que representa o ponto de partida para o estado antes da execução da ação.
  • permalink é uma string opcional que contém a URL única da página que o formulário modifica.
Para usar o hook useActionState, certifique-se de que você tenha uma action configurada primeiro. Vamos usar a ação do exemplo anterior para isso, com uma pequena variação:
"use server";

export async function submitForm(_, formData) {
  const name = formData.get("name");

  const hour = new Date().getHours();
  let greeting;

  if (hour < 12) {
    greeting = "Good morning";
  } else if (hour < 18) {
    greeting = "Good afternoon";
  } else {
    greeting = "Good evening";
  }

  return { message: ${greeting}, ${name} };
}
No seu componente, você precisa importar o hook useActionState e chamá-lo no nível superior do corpo do componente (antes da declaração return) assim como outros hooks. Você também deve importar a action:
"use client";

// Import the useActionState hook
import { useActionState } from "react";

// Import the submitForm action
import { submitForm } from "./actions/submitForm";

const Greeter = () => {

 // Initialize the hook
 const [state, submit, isPending] = useActionState(submitForm, {
   message: "",
 });

  return (
    <div className="flex flex-col items-center justify-center min-h-screen bg-gray-100 p-6">
      {/* Rest of component */}
    </div>
  );
};

export default Greeter;
Aqui está como o código completo fica com um pouco de estilo:
"use client";

import { useActionState } from "react";
import { submitForm } from "./actions/submitForm";

const Greeter = () => {
  const [state, submit, isPending] = useActionState(submitForm, {
    message: "",
  });

  return (
    <div className="flex flex-col items-center justify-center min-h-screen bg-gray-100 p-6">
      <form
        action={submit}
        className="bg-white p-6 rounded-2xl shadow-md w-full max-w-md"
      >
        <h2 className="text-2xl text-center font-semibold text-gray-700 mb-4">
          Greet Someone
        </h2>

        <input
          type="text"
          name="name"
          placeholder="Enter your name"
          required
          className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-400"
        />

        <button
          type="submit"
          disabled={isPending}
          className="w-full mt-4 p-3 bg-green-500 text-white font-semibold rounded-lg hover:bg-green-600 disabled:bg-gray-400 transition-all"
        >
          {isPending ? "Greeting..." : "Greet"}
        </button>

        {state.message && (
          <p className="mt-4 text-green-600 text-center font-medium">
            {state.message}
          </p>
        )}
      </form>
    </div>
  );
};

export default Greeter;
No navegador, você veria o botão do seu formulário mudar de Greet para Greeting... enquanto a ação isPending está em andamento - e a saudação mostraria Good morning, {name}, Good afternoon, {name} ou Good evening, {name}, dependendo da hora do dia em que o formulário foi enviado. Lembra como mencionamos que você também pode usar o hook useActionState fora de um formulário? Neste exemplo, vamos buscar cinco usuários do JSONPlaceholder com um clique no botão:
"use server";

export async function getUsers() {
  const res = await fetch(
    "https://jsonplaceholder.typicode.com/users?_start=0&_limit=5/"
  );
  return await res.json();
}
Aqui está a interface estilizada:
"use client";

import { useActionState } from "react";
import { getUsers } from "./actions/getUsers";

export default function FetchUsers() {
  const [users, fetchAction, isPending] = useActionState(getUsers, []);

  return (
    <div className="p-6 max-w-lg mx-auto">
      <button
        onClick={fetchAction}
        disabled={isPending}
        className="px-4 py-2 cursor-pointer bg-green-500 text-white rounded-lg hover:bg-green-600 disabled:bg-gray-400 font-bold"
      >
        {isPending ? "Fetching Users..." : "Fetch Users"}
      </button>

      <ul className="mt-4 space-y-2">
        {users.map((user) => (
          <li key={user.id} className="p-3 bg-gray-100 rounded-lg">
            <p className="font-semibold">{user.name}</p>
            <p className="text-sm text-gray-600">{user.email}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}
No navegador, você veria que o texto do botão nunca é atualizado para Fetching Users... depois que ele é clicado. Isso acontece porque o React trata a busca de dados e a renderização como uma prioridade maior do que o estado isPending, que bloqueia o isPending no processo e gera um erro. Para corrigir esse problema, você precisa envolver a ação em startTransition:
"use client";

// import startTransition from React
import { useActionState, startTransition } from "react";
import { getUsers } from "./actions/getUsers";

export default function FetchUsers() {
  const [users, fetchAction, isPending] = useActionState(getUsers, []);

  return (
    <div className="p-6 max-w-lg mx-auto">
      <button
        {/* wrap fetchAction in startTransition */}
        onClick={() => startTransition(() => fetchAction())}
        disabled={isPending}
        className="px-4 py-2 bg-green-500 font-bold cursor-pointer text-white rounded-lg hover:bg-green-600 disabled:bg-gray-400"
      >
        {isPending ? 'Fetching Users...' : 'Fetch Users'}
      </button>

      <ul className="mt-4 space-y-2">
        {users.map((user) => (
          <li key={user.id} className="p-3 bg-gray-100 rounded-lg">
            <p className="font-semibold">{user.name}</p>
            <p className="text-sm text-gray-600">{user.email}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}
Se você está se perguntando o que é startTransition, é uma função que informa ao React que uma atualização de estado é de baixa prioridade e pode ser interrompida. Isso mantém a interface do usuário responsiva enquanto lida com atualizações assíncronas como server actions. É assim que se usa o hook useActionState dentro e fora de um formulário.
Este módulo não possui perguntas. Marque como concluído.