Lidar com formulários em React sempre exigiu o mesmo bloco de código repetido: estado para o valor, estado para o erro, estado para o loading, e um onSubmit para orquestrar tudo isso com try/catch. O hook useActionState, estabilizado no React 19, assume esse trabalho: ele conecta uma função assíncrona (uma Action) ao estado de um componente e devolve o resultado mais recente, uma versão "empacotada" da função e um indicador de pendência, tudo pronto para usar.
O jeito antigo
Antes do React 19, um formulário simples já precisava de três useState e um manipulador cuidando de cada etapa manualmente:
function FormularioEmail() {
const [email, setEmail] = useState('');
const [erro, setErro] = useState(null);
const [enviando, setEnviando] = useState(false);
async function handleSubmit(e) {
e.preventDefault();
setEnviando(true);
setErro(null);
try {
await inscrever(email);
} catch (e) {
setErro(e.message);
} finally {
setEnviando(false);
}
}
return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={e => setEmail(e.target.value)} />
<button disabled={enviando}>Inscrever</button>
{erro && <p>{erro}</p>}
</form>
);
}
Como funciona o useActionState
O hook recebe uma função action — que agora passa a receber o estado anterior como primeiro argumento — e um estado inicial, e devolve três valores: o estado atual, a Action já conectada para disparar e um booleano de pendência:
const [estado, formAction, isPending] = useActionState(
async (estadoAnterior, formData) => {
const email = formData.get('email');
try {
await inscrever(email);
return { erro: null };
} catch (e) {
return { erro: e.message };
}
},
{ erro: null }
);
O formAction retornado vai direto no atributo action da tag <form>. O React se encarrega de chamar a função, passar um objeto FormData com os campos do formulário e atualizar o estado com o que ela retornar — sem onSubmit, sem preventDefault() e sem estado manual de erro:
function FormularioEmail() {
const [estado, formAction, isPending] = useActionState(
async (_, formData) => {
try {
await inscrever(formData.get('email'));
return { erro: null };
} catch (e) {
return { erro: e.message };
}
},
{ erro: null }
);
return (
<form action={formAction}>
<input name="email" type="email" required />
<button disabled={isPending}>Inscrever</button>
{estado.erro && <p>{estado.erro}</p>}
</form>
);
}
Exemplo prático: contador com histórico
useActionState não é exclusivo de formulários — qualquer Action, mesmo chamada fora de um <form>, pode usar o estado anterior para acumular resultado. Aqui, cada clique soma ao total anterior:
function Contador() {
const [total, incrementar, isPending] = useActionState(
async (totalAnterior, incremento) => {
await new Promise(r => setTimeout(r, 300));
return totalAnterior + incremento;
},
0
);
return (
<button onClick={() => incrementar(1)} disabled={isPending}>
Total: {total}
</button>
);
}
Note que, aqui, a Action é disparada em um onClick comum — incrementar(1) chama a versão conectada pelo hook, passando 1 como segundo argumento da função original.
Progressive enhancement com permalink
Quando o formulário é renderizado no servidor (Server-Side Rendering) e o JavaScript ainda não carregou no cliente, o terceiro argumento opcional permalink informa para qual URL o navegador deve enviar o formulário de forma nativa, preservando o comportamento mesmo antes da hidratação:
const [estado, formAction] = useActionState(atualizarPerfil, null, '/perfil');
Quando usar
- Formulários que dependem do resultado de uma submissão (sucesso, erro de validação, dados atualizados).
- Qualquer Action assíncrona que precise de um indicador de pendência (
isPending) sem gerenciar esse booleano manualmente. - Fluxos onde o próximo estado depende do estado anterior, como contadores, filas ou acumuladores disparados por eventos.
- Combinado com
useFormStatusdentro dos campos do formulário, para refletir o estado de pendência em componentes filhos sem precisar repassar props.
Cuidados
A função passada para useActionState precisa sempre retornar o novo estado — inclusive em caso de erro tratado dentro da própria função, já que lançar uma exceção não tratada propaga para o error boundary mais próximo em vez de atualizar o estado. Além disso, o primeiro argumento da Action é sempre o estado anterior, o que muda a assinatura de uma função existente caso você esteja migrando um onSubmit já pronto para esse padrão.
Compatibilidade
useActionState foi introduzido como useFormState nos canais experimentais do React e renomeado e estabilizado no React 19 (dezembro de 2024), disponível tanto em react quanto reexportado por react-dom. Funciona com React DOM e também com React Server Components, sem flag ou configuração extra a partir dessa versão.
Conclusão
O useActionState elimina o trio clássico de useState para valor, erro e loading em torno de uma submissão, substituindo-o por um único hook que entende o ciclo de vida completo de uma Action. O resultado é menos código repetido e formulários que funcionam de forma consistente, com ou sem JavaScript totalmente carregado no cliente.
0 Comentário
Deixe seu Comentário!
Dúvida, sugestão ou só um "valeu!" — tudo é bem-vindo por aqui.