UseOptimistic e useActionState do React 19: substituindo 80% do padrão do seu estado


Reaja aos 19 useOptimistic e useActionState juntos eliminam as partes mais repetitivas dessa cerimônia: sinalizadores de carregamento manual, estado de erro e lógica de reversão otimista. Esses dois ganchos estáveis ​​e de primeira classe lidam com atualizações otimistas da interface do usuário com reversão automática e gerenciamento de estado de ação de formulário nativo, desmoronando o que antes era uma parede de clichê em aproximadamente 12 linhas declarativas.

Índice

O problema padrão no gerenciamento do estado de reação

Um típico envio de formulário ou mutação de lista no React exige há muito tempo uma cerimônia previsível e tediosa: uma useState chamada para dados, outra para carregamento, uma terceira para erro, um manipulador assíncrono envolvido em try/catch/finallye muitas vezes um useEffect para limpeza. Adicione atualizações otimistas à interface do usuário e o quadro piorará. Os desenvolvedores capturam o estado antes da mutação, aplicam a atualização com entusiasmo e revertem manualmente em caso de falha. Para um único recurso, isso atinge facilmente de 30 a 50 linhas de encanamento mecânico.

Reaja aos 19 useOptimistic e useActionState juntos eliminam as partes mais repetitivas dessa cerimônia: sinalizadores de carregamento manual, estado de erro e lógica de reversão otimista. Esses dois ganchos estáveis ​​e de primeira classe lidam com atualizações otimistas da interface do usuário com reversão automática e gerenciamento de estado de ação de formulário nativo, desmoronando o que antes era uma parede de clichê em aproximadamente 12 linhas declarativas.

Pré-requisitos para os exemplos a seguir: familiaridade com ganchos React, um entendimento básico de funções assíncronas (ou ações de servidor em Next.js) e um ambiente Node.js (Node 18 ou posterior recomendado).

O que mudou no modelo de estado do React 19

De máquinas de estado manuais a ações declarativas

O React 19 introduz o conceito de “ações”, funções assíncronas que se integram diretamente ao sistema de transição do React. Em vez de orquestrar manualmente as transições de estado em vários useState e useEffect chamadas, os desenvolvedores passam uma função assíncrona para o React e permitem que a estrutura gerencie estados pendentes, serialização e reconciliação.

Dois ganchos ficam no centro deste modelo. useActionState substitui o experimental useFormState de react-dom construções canárias. Importado de react (não react-dom), acrescenta isPending como um terceiro valor de retorno e gerencia o ciclo de vida de um formulário ou ação imperativa: seu resultado, seu erro e seu status pendente. useOptimistic lida com a preocupação complementar de mostrar uma atualização imediata da interface do usuário que é revertida automaticamente quando o trabalho assíncrono subjacente é resolvido ou falha.

Esses ganchos são diferentes de soluções de terceiros, como React Query, SWR ou Redux Toolkit. Eles têm como alvo o estado de ação local da UI, não a sincronização global do cache do servidor. Uma mutação que precisa de invalidação de cache em vários componentes ainda se beneficia dessas bibliotecas. Mas para o padrão de envio e resposta com escopo de componente que domina a maioria dos aplicativos, os ganchos integrados eliminam a necessidade de dependências externas.

Notas de compatibilidade e adoção

Ambos os ganchos requerem React 19.0.0 estável como versão mínima. Eles funcionam com React DOM e React Native. Para aplicativos Next.js, useActionState funciona diretamente com ações do servidor. Para aplicativos puramente do lado do cliente, qualquer função assíncrona funciona como ação. React Native pode usar useActionState com apelos a ações imperativas, mas o pattern is React DOM-specific.

To install:

npm install react@19 react-dom@19

Entendimento useActionState

Assinatura API e modelo mental

Importe o gancho de react:

import { useActionState } from 'react';

const (state, formAction, isPending) = useActionState(actionFn, initialState, permalink?)

Três valores voltam. state é o resultado acumulado da invocação de ação mais recente, começando como initialState. formAction é uma função vinculada que você passa diretamente para um ‘s action prop ou call imperativamente. isPending é um booleano que é true enquanto a ação está em andamento.

Este único gancho substitui o trio comum de useState chamadas (para resultado/erro, para carregamento) e o try/catch/finally padrão dentro de um manipulador de envio.

Este único gancho substitui o trio comum de useState chamadas (para resultado/erro, para carregamento) e o try/catch/finally padrão dentro de um manipulador de envio.

Antes: manipulador tradicional de envio de formulários

import { useState } from 'react';

function ContactForm() {
  const (data, setData) = useState(null);
  const (error, setError) = useState(null);
  const (isLoading, setIsLoading) = useState(false);

  async function handleSubmit(e) {
    e.preventDefault();
    setIsLoading(true);
    setError(null);

    try {
      const formData = new FormData(e.target);
      const res = await fetch('/api/contact', {
        method: 'POST',
        body: formData,
      });

      if (!res.ok) throw new Error('Submission failed');

      const result = await res.json();
      setData(result);
    } catch (err) {
      setError(err.message);
    } finally {
      setIsLoading(false);
    }
  }

  return (
    form onSubmit={handleSubmit}>
      input name="email" required />
      button disabled={isLoading}>{isLoading ? 'Sending...' : 'Send'}button>
      {error && p className="error">{error}p>}
      {data && p>Thanks! We received your message.p>}
    form>
  );
}

Depois: Mesma forma com useActionState

O submitContact A função mostrada abaixo deve ser definida no mesmo módulo (ou importado) antes do componente.

import { useActionState } from 'react';

async function submitContact(prevState, formData) {
  const email = formData.get('email');

  if (!email || !/^(^\s@)+@(^\s@)+\.(^\s@)+$/.test(email)) {
    return { success: false, error: 'Please enter a valid email', data: null };
  }

  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 8000);

  let res;
  try {
    res = await fetch('/api/contact', {
      method: 'POST',
      body: formData,
      signal: controller.signal,
    });
  } catch (err) {
    return {
      success: false,
      error: err.name === 'AbortError' ? 'Request timed out.' : 'Network error.',
      data: null,
    };
  } finally {
    clearTimeout(timeoutId);
  }

  if (!res.ok) {
    return { success: false, error: 'Server error. Please try again.', data: null };
  }

  const result = await res.json();
  return { success: true, error: null, data: result };
}

function ContactForm() {
  const (state, formAction, isPending) = useActionState(submitContact, {
    data: null,
    error: null,
  });

  return (
    form action={formAction}>
      input name="email" required />
      button disabled={isPending}>{isPending ? 'Sending...' : 'Send'}button>
      {state.error && !isPending && p className="error" role="alert">{state.error}p>}
      {state.data && p>Thanks! We received your message.p>}
    form>
  );
}

As muitas linhas de controle do estado caem para cerca de 12 dentro do componente. Não onSubmitnão preventDefaultsem alternância de carregamento manual.

Como funciona a função de ação

A função de ação segue uma assinatura semelhante a um redutor:

async (previousState, formData) => nextState

React passa o estado acumulado atual e o FormData a partir do envio do formulário. A função retorna o próximo estado. React serializa envios quando você invoca ações por meio formAction ou um useActionStatemanipulador vinculado. O React não serializa chamadas feitas fora do seu sistema de transição. A integração com is automatic, so there is no need for onSubmit ou preventDefault.

Tratamento de erros sem Try/Catch

Como a função de ação retorna o estado em vez de lançar, o tratamento de erros torna-se uma questão de retornar uma forma diferente. O submitContact A função acima demonstra este padrão: erros de validação, erros de servidor e sucesso retornam um objeto que flui diretamente para state. Nenhuma variável de estado de erro separada, nenhum bloco catch no componente.

Entendimento useOptimistic

Assinatura API e modelo mental

A assinatura do gancho é:

import { useOptimistic } from 'react';

const (optimisticState, addOptimistic) = useOptimistic(state, updateFn)

O primeiro argumento, stateé a fonte canônica da verdade, normalmente de adereços, do estado de um componente pai ou de uma resposta do servidor. addOptimistic é uma função que aciona uma atualização imediata da IU. Quando a ação assíncrona que envolve a chamada otimista é concluída (seja bem-sucedida ou falha), o React reconcilia automaticamente optimisticState de volta ao que quer que seja state atualmente detém.

O mecanismo de reversão automática

O insight principal é que useOptimistic vincula seu ciclo de vida ao sistema de transição do React. Quando a transição React que desencadeou addOptimistic completa, optimisticState resolve de volta ao canônico state valor. A ação deve ser executada dentro de uma transição – via , useActionStateou explícito startTransition – para que essa reversão ocorra. Se o servidor confirmar a mutação, state refletirá os novos dados, portanto a atualização otimista persiste naturalmente. Se o servidor rejeitar, state permanece inalterado e a atualização otimista desaparece. Sem instantâneo manual, sem reversão manual, sem efeitos de limpeza.

Se o servidor rejeitar, state permanece inalterado e a atualização otimista desaparece. Sem instantâneo manual, sem reversão manual, sem efeitos de limpeza.

Antes: atualização otimista manual com reversão

import { useState } from 'react';

function TodoList({ initialTodos }) {
  const (todos, setTodos) = useState(initialTodos);
  const (isLoading, setIsLoading) = useState(false);
  const (error, setError) = useState(null);

  async function addTodo(text) {
    const snapshot = (...todos);
    const tempTodo = { id: Date.now(), text, pending: true };
    setTodos((prev) => (...prev, tempTodo));
    setIsLoading(true);
    setError(null);

    try {
      const res = await fetch('/api/todos', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text }),
      });

      if (!res.ok) throw new Error('Failed to add todo');

      const saved = await res.json();
      setTodos((prev) => prev.map((t) => (t.id === tempTodo.id ? saved : t)));
    } catch (err) {
      setTodos(snapshot);
      setError(err.message);
    } finally {
      setIsLoading(false);
    }
  }

  return (
    div>
      {error && p className="error">{error}p>}
      ul>{todos.map((t) => li key={t.id}>{t.text}li>)}ul>
      button onClick={() => addTodo('New task')} disabled={isLoading}>Addbutton>
    div>
  );
}

Depois: mesmo recurso com useOptimistic

import { useOptimistic } from 'react';


function TodoList({ todos, addTodoAction }) {
  const (optimisticTodos, addOptimisticTodo) = useOptimistic(
    todos,
    (currentTodos, newTodo) => (...currentTodos, newTodo)
  );

  async function handleAdd(formData) {
    const text = formData.get('text');
    addOptimisticTodo({ id: crypto.randomUUID(), text, pending: true });

    try {
      await addTodoAction(text);
    } catch (err) {
      
      console.error('Failed to add todo:', err);
    }
  }

  return (
    div>
      ul>{optimisticTodos.map((t) => li key={t.id}>{t.text}li>)}ul>
      form action={handleAdd}>
        input name="text" required />
        button type="submit">Addbutton>
      form>
    div>
  );
}

As 30 linhas da lógica de instantâneo e reversão são reduzidas para aproximadamente 12. A reversão em caso de falha é automática.

Funções de atualização personalizadas

Sempre forneça o updateFn argumento – define o comportamento de mesclagem. Ele recebe (currentState, optimisticValue) e retorna o novo estado otimista. Isso permite que os desenvolvedores controlem como o valor otimista se funde: anexando a uma matriz, alternando um campo booleano, incrementando um contador ou qualquer outra transformação.

Combinando os dois ganchos: exemplo Full-Stack Todo

Configuração do projeto

O exemplo a seguir usa React 19 no cliente e um endpoint mínimo da API Express/Node.js em POST /api/todos. O servidor simula um atraso de rede de 1 segundo e retorna aleatoriamente um erro 500 em aproximadamente 30% das vezes (em ambientes que não sejam de produção), o que facilita a observação do comportamento de reversão.

Garantir express.json() o middleware é registrado antes da rota para analisar o corpo da solicitação JSON.

Endpoint do servidor (server.js):

const express = require('express');
const app = express();

const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN || 'http://localhost:5173';

app.use(express.json()); 

app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', ALLOWED_ORIGIN);
  res.header('Access-Control-Allow-Headers', 'Content-Type');
  res.header('Access-Control-Allow-Methods', 'POST, OPTIONS');

  if (req.method === 'OPTIONS') return res.sendStatus(204);

  next();
});

app.post('/api/todos', async (req, res, next) => {
  try {
    const { text } = req.body;

    if (typeof text !== 'string' || text.trim().length === 0 || text.length > 500) {
      return res.status(400).json({ error: 'Invalid text' });
    }

    await new Promise((resolve) => setTimeout(resolve, 1000));

    if (process.env.NODE_ENV !== 'production' && Math.random()  0.3) {
      return res.status(500).json({ error: 'Random server failure' });
    }

    const todo = { id: Date.now(), text: text.trim() };
    res.json(todo);
  } catch (err) {
    next(err);
  }
});

app.listen(3000, () => console.log('Server running on port 3000'));

Observação: Se o seu servidor de desenvolvimento React for executado em uma porta diferente (por exemplo, 5173 para Vite), defina o ALLOWED_ORIGIN variável de ambiente para corresponder à origem do seu servidor de desenvolvimento. O middleware CORS acima restringe o acesso a uma única origem permitida em vez de usar um curinga, o que é importante para a segurança nos terminais de mutação.

Instale a dependência do servidor separadamente:

npm install express

Construindo o Componente

O componente abaixo usa useOptimistic para feedback instantâneo da interface do usuário e useActionState para gerenciar o ciclo de vida do envio, incluindo estado pendente e exibição de erros. A função de ação retorna a lista de todos atualizada como parte do estado da ação, evitando o risco de simultaneidade de chamar setTodos de dentro de um useActionState Ação.

Chamar addOptimisticTodo antes de qualquer await expressão na ação. O sistema de transição do React captura apenas atualizações otimistas emitidas de forma síncrona antes do primeiro ponto de suspensão.

import { useOptimistic, useActionState } from 'react';

export default function TodoList() {
  
  async function todoAction(prevState, formData) {
    const text = formData.get('text');
    const tempTodo = { id: crypto.randomUUID(), text, pending: true };

    
    addOptimisticTodo(tempTodo);

    let res;
    try {
      res = await fetch('/api/todos', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text }),
      });
    } catch {
      return { error: 'Network error. Please try again.', todos: prevState.todos };
    }

    if (!res.ok) {
      const errBody = await res.text();
      console.error('Todo API error:', res.status, errBody);
      
      return { error: 'Failed to add todo. Please try again.', todos: prevState.todos };
    }

    const savedTodo = await res.json();
    
    return { error: null, todos: (...prevState.todos, savedTodo) };
  }

  
  const (state, formAction, isPending) = useActionState(todoAction, {
    error: null,
    todos: (),
  });

  
  const (optimisticTodos, addOptimisticTodo) = useOptimistic(
    state.todos,
    (current, newTodo) => (...current, newTodo)
  );

  return (
    div>
      {state.error && !isPending && (
        p className="error" role="alert">{state.error}p>
      )}
      ul>
        {optimisticTodos.map((t) => (
          li key={t.id} style={{ opacity: t.pending ? 0.5 : 1 }}>{t.text}li>
        ))}
      ul>
      form action={formAction}>
        input name="text" required />
        button type="submit" disabled={isPending}>
          {isPending ? 'Adding...' : 'Add Todo'}
        button>
      form>
    div>
  );
}

O que acontece em caso de falha: passo a passo

A sequência é: o usuário envia o formulário. useActionState envolve a ação em uma transição React automaticamente. A tarefa otimista aparece instantaneamente na lista com opacidade reduzida (pending: true). Um segundo depois, o servidor retorna um erro 500. A função de ação retorna { error: 'Failed to add todo. Please try again.', todos: prevState.todos } com a lista de todos anterior inalterada. Quando a transição for concluída, o React reconcilia optimisticState de volta ao inalterado todose o item otimista desaparece da lista. A mensagem de erro é renderizada. Código de reversão manual zero.

Lista de verificação de implementação e guia de migração

Quando alcançar cada gancho

CenárioGanchoSubstitui
Envio de formulário com carregamento/errouseActionStateuseState x3 + try/catch manipulador
Feedback instantâneo da UI antes da confirmação do servidoruseOptimisticInstantâneo manual + lógica de reversão
Ambos (enviar + feedback instantâneo)Ambos juntos40-50 linhas de lógica personalizada
Sincronização global do cache do servidorNenhum; usar React Query/SWRN / D

Lista de verificação de migração

Auditoria

  1. Confirme React 19.0.0 ou posterior em package.json (npm install react@19 react-dom@19).
  2. Identifique os componentes com o manual isLoading / error / data trios estaduais.
  3. Identifique padrões de atualização otimistas em que os instantâneos de código estão antes da mutação e são revertidos em caso de falha.

Substituir

  1. Substitua os manipuladores de envio por useActionState funções de ação usando o async (prevState, formData) => nextState assinatura. Importar useActionState de react.
  2. Substituir onSubmit com (React DOM only).
  3. Remove e.preventDefault() chamadas.
  4. Substitua os padrões de instantâneo e reversão por useOptimisticpassando o estado canônico como primeiro argumento e sempre fornecendo um updateFn.
  5. Envolva chamadas otimistas dentro da função de ação ou startTransition. Se estiver usando useActionStatea ação já está envolvida em uma transição. Use apenas explícito startTransition ao ligar addOptimistic fora de um useActionState ação ou manipulador de formulário.
  6. Remover reversão manual catch blocos.

Teste

  1. Teste explicitamente os caminhos de falha e confirme o comportamento de reversão automática.

Pegadinhas e Limitações

Coisas a observar

useActionState serializa envios. Fila de cliques duplos rápidos em vez de corrida, o que evita a corrupção de dados, mas significa que esta não é a ferramenta certa quando mutações paralelas são realmente necessárias.

useOptimistic só reverte quando o canônico state alterações de referência. Se uma ação falhar silenciosamente, mas nunca atualizar o estado passado para useOptimistico valor otimista persiste indefinidamente. Sempre retorne o novo estado da ação, mesmo em caso de falha, ou certifique-se de que a variável de estado canônico reflita o verdadeiro estado do servidor.

A causa mais comum de um item otimista persistente é ligar addOptimistic fora de uma transição React (por exemplo, em uma versão simples setTimeout ou um manipulador de eventos sem transição). Garanta tudo addOptimistic chamadas ocorrem dentro startTransition, useActionStateação de um formulário ou action negociações de suporte.

O permalink parâmetro em useActionState existe para aprimoramento progressivo em contextos renderizados pelo servidor (fallback SSR/no-JS) e também é usado pelo Remix/React Router v7 para vinculação de URL de formulário. Omita-o em aplicativos somente SPA.

Esses ganchos não substituem o gerenciamento de estado global ou as bibliotecas de cache do servidor. Eles têm como alvo fluxos de ação local de componentes. Para invalidação de cache entre componentes, sincronização de estado do servidor ou busca em segundo plano, React Query, SWR e bibliotecas semelhantes continuam sendo a escolha apropriada.

Esses ganchos não substituem o gerenciamento de estado global ou as bibliotecas de cache do servidor. Eles têm como alvo fluxos de ação local de componentes.

Escreva recursos, não encanamento

useActionState elimina o padrão de carregamento, erro e envio. useOptimistic elimina a lógica de instantâneo e reversão. Juntos, eles cobrem a grande maioria dos padrões de estado interativos que os desenvolvedores constroem componente após componente. Auditar um componente de formulário existente e migrá-lo usando a lista de verificação acima consome cerca de 40 a 50 linhas de manual isLoading/error/data gerenciamento de estado e lógica de reversão baseada em instantâneo até cerca de 12.

A documentação oficial do React 19 para useActionState e usarOtimista fornece detalhes adicionais sobre casos extremos e padrões de uso avançados.



Source link