O que Todo Dev Deve Saber sobre useTransition e useOptimistic: UX de Alta Performance em React 18 Já leu

Introdução ao Problema de Performance em React Quando construímos aplicações web modernas, uma das maiores reclamações dos usuários não é sobre funcionalidade, mas sobre responsividade. Você já clicou em um botão e sentiu aquele lag frustrante? Aquele atraso entre a ação e a reação visual? Isso acontece porque o React precisa processar a atualização de estado, renderizar novos componentes e atualizar o DOM — tudo isso acontece no mesmo thread que executa JavaScript, CSS e responde a eventos do usuário. O React 18 introduziu dois hooks revolucionários para resolver esse problema de forma elegante: e . Eles permitem que você marque atualizações como "não urgentes", liberando o thread principal para responder a interações críticas como cliques e digitação. Neste artigo, você aprenderá não apenas como usá-los, mas por que funcionam e quando realmente faz diferença. useTransition: Priorização de Atualizações de Estado O Conceito Fundamental permite que você marque uma atualização de estado como uma transição — uma operação que pode

Introdução ao Problema de Performance em React

Quando construímos aplicações web modernas, uma das maiores reclamações dos usuários não é sobre funcionalidade, mas sobre responsividade. Você já clicou em um botão e sentiu aquele lag frustrante? Aquele atraso entre a ação e a reação visual? Isso acontece porque o React precisa processar a atualização de estado, renderizar novos componentes e atualizar o DOM — tudo isso acontece no mesmo thread que executa JavaScript, CSS e responde a eventos do usuário.

O React 18 introduziu dois hooks revolucionários para resolver esse problema de forma elegante: useTransition e useOptimistic. Eles permitem que você marque atualizações como "não urgentes", liberando o thread principal para responder a interações críticas como cliques e digitação. Neste artigo, você aprenderá não apenas como usá-los, mas por que funcionam e quando realmente faz diferença.

useTransition: Priorização de Atualizações de Estado

O Conceito Fundamental

useTransition permite que você marque uma atualização de estado como uma transição — uma operação que pode ser interrompida e resumida sem prejudicar a experiência do usuário. Ao invés de travar o navegador enquanto processa algo pesado, React pausa o trabalho, processa eventos do usuário que chegam, e depois continua.

O hook retorna dois valores: uma função startTransition que você chama com código que atualiza estado, e um booleano isPending que indica se a transição está em progresso. Isso permite que você mostre feedback visual enquanto o React trabalha nos bastidores.

Exemplo Prático: Filtro de Lista Pesada

Imagine uma aplicação que filtra uma lista de 10 mil itens enquanto o usuário digita. Sem useTransition, a interface congela. Com ele, a digitação permanece responsiva:

import { useState, useTransition } from 'react';

export function FilterableList() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  const items = Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `Item ${i}`,
    description: `Description for item ${i}`
  }));

  const filteredItems = items.filter(item =>
    item.name.toLowerCase().includes(query.toLowerCase())
  );

  const handleInputChange = (e) => {
    const value = e.target.value;
    // Atualização urgente: o input responde imediatamente
    setQuery(value);

    // Atualização não urgente: filtragem acontece em background
    startTransition(() => {
      // Aqui você poderia atualizar outro state
      // que depende da filtragem pesada
    });
  };

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleInputChange}
        placeholder="Digite para filtrar..."
      />

      {isPending && <p>Filtrando...</p>}

      <ul>
        {filteredItems.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

Observe que setQuery(value) acontece fora de startTransition. Isso é intencional — queremos que o input responda imediatamente ao usuário. Se você quiser, pode colocar a filtragem em um estado separado dentro de startTransition, mas neste caso simples, o React otimiza automaticamente.

Padrão Avançado: Múltiplos Estados com Transição

Quando você tem lógica mais complexa, vale a pena separar claramente qual estado é urgente e qual não:

import { useState, useTransition } from 'react';

export function SearchWithResults() {
  const [inputValue, setInputValue] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleSearch = (value) => {
    // Urgente: atualiza o input imediatamente
    setInputValue(value);

    // Não urgente: calcula e renderiza resultados em background
    startTransition(() => {
      // Simulando busca pesada (em produção, seria uma API call)
      const filtered = simulateExpensiveSearch(value);
      setResults(filtered);
    });
  };

  return (
    <div>
      <input
        value={inputValue}
        onChange={(e) => handleSearch(e.target.value)}
        placeholder="Buscar..."
      />

      {isPending && <div className="spinner">Carregando resultados...</div>}

      <ul className={isPending ? 'opacity-50' : ''}>
        {results.map(result => (
          <li key={result.id}>{result.title}</li>
        ))}
      </ul>
    </div>
  );
}

function simulateExpensiveSearch(query) {
  // Simulação de operação pesada
  const start = performance.now();
  while (performance.now() - start < 500) {}

  return Array.from({ length: 50 }, (_, i) => ({
    id: i,
    title: `Resultado: ${query} - ${i}`
  }));
}

useOptimistic: Feedback Imediato com Sincronização Segura

Quando Você Precisa Adivinhar o Futuro

useOptimistic resolve um problema diferente: quando você envia uma ação para o servidor (POST, PUT, DELETE), o usuário quer ver o resultado imediatamente, mas você só terá a confirmação em alguns milissegundos ou segundos. Se você esperar a resposta do servidor, a interface parece lenta. Se você atualizar o estado antes da confirmação e a requisição falhar, fica confuso.

A solução é atualizar o estado otimisticamente — mostrar o resultado esperado enquanto a requisição está em progresso, e fazer rollback se falhar. useOptimistic torna isso seguro e simples.

Exemplo Prático: Like Button

Vamos implementar um botão de like que atualiza a contagem imediatamente, mesmo esperando a resposta do servidor:

import { useOptimistic, useState } from 'react';

export function PostWithLike() {
  const [post, setPost] = useState({
    id: 1,
    title: 'Meu Primeiro Post',
    likes: 42,
    liked: false
  });

  const [optimisticPost, addOptimisticLike] = useOptimistic(
    post,
    (currentPost, newLikes) => ({
      ...currentPost,
      likes: newLikes,
      liked: !currentPost.liked
    })
  );

  const handleLike = async () => {
    // Atualiza otimisticamente
    addOptimisticLike(optimisticPost.liked ? post.likes - 1 : post.likes + 1);

    try {
      // Envia para o servidor
      const response = await fetch(`/api/posts/${post.id}/like`, {
        method: 'POST'
      });

      const updatedPost = await response.json();

      // Sincroniza com a resposta real do servidor
      setPost(updatedPost);
    } catch (error) {
      // Se falhar, o estado otimístico é descartado automaticamente
      // e volta ao valor original
      console.error('Erro ao fazer like:', error);
    }
  };

  return (
    <div className="post">
      <h2>{optimisticPost.title}</h2>
      <button 
        onClick={handleLike}
        className={optimisticPost.liked ? 'liked' : ''}
      >
        ❤️ {optimisticPost.likes}
      </button>
    </div>
  );
}

Veja como funciona:

  1. Você clica no botão
  2. O estado muda imediatamente — o contador aumenta e a cor muda
  3. Em paralelo, a requisição HTTP é enviada
  4. Se a resposta vier com sucesso, setPost sincroniza o estado real
  5. Se falhar, o React automaticamente reverte para o estado anterior sem você fazer nada

Padrão Avançado: Múltiplas Ações Otimistas

Em aplicações reais, você pode ter múltiplas ações pendentes. useOptimistic permite atualizações em fila:

import { useOptimistic, useState } from 'react';

export function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, title: 'Aprender React 18', completed: false }
  ]);

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

  const handleAddTodo = async (title) => {
    const tempTodo = {
      id: Date.now(),
      title,
      completed: false
    };

    // Mostra o novo todo imediatamente
    addOptimisticTodo(tempTodo);

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

      const createdTodo = await response.json();

      // Substitui o otimista pelo real (com ID real do servidor)
      setTodos(current =>
        current.map(t => t.id === tempTodo.id ? createdTodo : t)
      );
    } catch (error) {
      console.error('Erro ao criar todo:', error);
      // Automaticamente reverte
    }
  };

  return (
    <div>
      <button onClick={() => handleAddTodo('Novo Todo')}>
        Adicionar
      </button>

      <ul>
        {optimisticTodos.map(todo => (
          <li key={todo.id} className={todo.completed ? 'completed' : ''}>
            {todo.title}
          </li>
        ))}
      </ul>
    </div>
  );
}

Combinando Ambos os Hooks: O Padrão Completo

Quando Usar Cada Um

useTransition é para atualizações de estado derivadas — quando mudar um estado causa cálculos pesados. useOptimistic é para interações com servidor — quando você quer mostrar resultado antes da confirmação. Frequentemente, você usa ambos na mesma aplicação, em contextos diferentes.

Mas existe um caso de uso poderoso onde eles trabalham juntos: formulários com validação assíncrona pesada. Você mostra feedback imediato (otimista) enquanto valida no servidor em background (transição).

Exemplo Prático: Formulário de Reserva

import { useState, useTransition, useOptimistic } from 'react';

export function BookingForm() {
  const [booking, setBooking] = useState({
    date: '',
    time: '',
    guests: 1,
    status: 'idle' // idle, submitting, success, error
  });

  const [optimisticBooking, addOptimisticBooking] = useOptimistic(
    booking,
    (current, updates) => ({ ...current, ...updates })
  );

  const [isPending, startTransition] = useTransition();

  const handleSubmit = async (e) => {
    e.preventDefault();

    // Atualiza otimisticamente com status de submissão
    addOptimisticBooking({ status: 'submitting' });

    // Processamento em background (pode ser pesado)
    startTransition(async () => {
      try {
        const response = await fetch('/api/bookings', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(booking)
        });

        if (response.ok) {
          const result = await response.json();
          setBooking(prev => ({
            ...prev,
            status: 'success',
            ...result
          }));
        } else {
          setBooking(prev => ({
            ...prev,
            status: 'error'
          }));
        }
      } catch (error) {
        setBooking(prev => ({
          ...prev,
          status: 'error'
        }));
      }
    });
  };

  const handleChange = (e) => {
    const { name, value } = e.target;
    setBooking(prev => ({
      ...prev,
      [name]: value
    }));
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="date"
        name="date"
        value={optimisticBooking.date}
        onChange={handleChange}
        disabled={optimisticBooking.status === 'submitting'}
      />

      <input
        type="time"
        name="time"
        value={optimisticBooking.time}
        onChange={handleChange}
        disabled={optimisticBooking.status === 'submitting'}
      />

      <input
        type="number"
        name="guests"
        min="1"
        max="10"
        value={optimisticBooking.guests}
        onChange={handleChange}
        disabled={optimisticBooking.status === 'submitting'}
      />

      <button type="submit" disabled={optimisticBooking.status === 'submitting'}>
        {optimisticBooking.status === 'submitting' ? 'Reservando...' : 'Reservar'}
      </button>

      {optimisticBooking.status === 'success' && (
        <p className="success">Reserva confirmada!</p>
      )}

      {optimisticBooking.status === 'error' && (
        <p className="error">Erro ao fazer reserva. Tente novamente.</p>
      )}

      {isPending && (
        <p className="info">Processando reserva...</p>
      )}
    </form>
  );
}

Neste exemplo:

  1. O usuário submete o formulário
  2. Imediatamente, o estado é atualizado otimisticamente com status: 'submitting'
  3. O botão desabilita e mostra "Reservando..."
  4. A requisição HTTP é enviada dentro de startTransition
  5. Se suceder, setBooking atualiza com a resposta real
  6. Se falhar, o estado reverte e mostra erro

Otimizações Práticas e Armadilhas Comuns

O Perigo do Uso Incorreto

Um erro comum é colocar toda a lógica dentro de startTransition. Lembre-se: queremos apenas adiar o que é não-urgente. Se você colocar setInput dentro de startTransition, o input lag volta.

// ❌ ERRADO: input fica lento
const handleChange = (e) => {
  startTransition(() => {
    setInputValue(e.target.value);  // Deve estar fora!
  });
};

// ✅ CORRETO: input rápido, cálculos em background
const handleChange = (e) => {
  setInputValue(e.target.value);  // Urgente
  startTransition(() => {
    setExpensiveResult(calculateSomething(e.target.value));  // Não urgente
  });
};

Medindo o Impacto Real

Para validar se você realmente precisa dessas otimizações, use as DevTools do React ou o Performance API nativo:

export function PerformanceMonitor({ children }) {
  return (
    <>
      <React.Profiler
        id="main"
        onRender={(id, phase, actualDuration) => {
          if (actualDuration > 16) { // ~60fps threshold
            console.warn(
              `Render lento em ${id}: ${actualDuration.toFixed(2)}ms`
            );
          }
        }}
      >
        {children}
      </React.Profiler>
    </>
  );
}

Compatibilidade com Server Components (React 19)

Se você estiver usando React 19 com Server Components, useOptimistic funciona em Client Components e pode trabalhar com Server Actions:

'use client';

import { useOptimistic } from 'react';

export function ClientComponent({ updateServerData }) {
  const [optimisticData, addOptimisticUpdate] = useOptimistic(
    null,
    (_, newValue) => newValue
  );

  const handleClick = async () => {
    addOptimisticUpdate('Loading...');
    await updateServerData();
  };

  return (
    <button onClick={handleClick}>
      {optimisticData || 'Click'}
    </button>
  );
}

Conclusão

Aprendemos que useTransition e useOptimistic são ferramentas fundamentais para construir aplicações React modernas com excelente experiência do usuário. useTransition resolve o problema de renderizações pesadas priorizando atualizações urgentes, enquanto useOptimistic resolve o problema de latência de rede mostrando feedback imediato sem sacrificar a segurança dos dados.

O aprendizado prático mais importante é simplicidade: não comece a usar esses hooks por usar. Meça se há realmente problemas de performance, identifique a causa (renderização ou requisição), e aplique a solução apropriada. Um useTransition bem colocado pode transformar uma interface que parecia ruim em excelente, com apenas 3 linhas de código.

Referências


Artigos relacionados