React & Frontend

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

15 min de leitura

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

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

<h2>Introdução ao Problema de Performance em React</h2>

<p>Quando construímos aplicações web modernas, uma das maiores reclamações dos usuários não é sobre funcionalidade, mas sobre <strong>responsividade</strong>. 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.</p>

<p>O React 18 introduziu dois hooks revolucionários para resolver esse problema de forma elegante: <code>useTransition</code> e <code>useOptimistic</code>. Eles permitem que você marque atualizações como &quot;não urgentes&quot;, 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 <strong>por que funcionam</strong> e quando realmente faz diferença.</p>

<h2>useTransition: Priorização de Atualizações de Estado</h2>

<h3>O Conceito Fundamental</h3>

<p><code>useTransition</code> permite que você marque uma atualização de estado como uma <strong>transição</strong> — 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.</p>

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

<h3>Exemplo Prático: Filtro de Lista Pesada</h3>

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

<pre><code class="language-jsx">import { useState, useTransition } from &#039;react&#039;;

export function FilterableList() {

const [query, setQuery] = useState(&#039;&#039;);

const [isPending, startTransition] = useTransition();

const items = Array.from({ length: 10000 }, (_, i) =&gt; ({

id: i,

name: Item ${i},

description: Description for item ${i}

}));

const filteredItems = items.filter(item =&gt;

item.name.toLowerCase().includes(query.toLowerCase())

);

const handleInputChange = (e) =&gt; {

const value = e.target.value;

// Atualização urgente: o input responde imediatamente

setQuery(value);

// Atualização não urgente: filtragem acontece em background

startTransition(() =&gt; {

// Aqui você poderia atualizar outro state

// que depende da filtragem pesada

});

};

return (

&lt;div&gt;

&lt;input

type=&quot;text&quot;

value={query}

onChange={handleInputChange}

placeholder=&quot;Digite para filtrar...&quot;

/&gt;

{isPending &amp;&amp; &lt;p&gt;Filtrando...&lt;/p&gt;}

&lt;ul&gt;

{filteredItems.map(item =&gt; (

&lt;li key={item.id}&gt;{item.name}&lt;/li&gt;

))}

&lt;/ul&gt;

&lt;/div&gt;

);

}</code></pre>

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

<h3>Padrão Avançado: Múltiplos Estados com Transição</h3>

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

<pre><code class="language-jsx">import { useState, useTransition } from &#039;react&#039;;

export function SearchWithResults() {

const [inputValue, setInputValue] = useState(&#039;&#039;);

const [results, setResults] = useState([]);

const [isPending, startTransition] = useTransition();

const handleSearch = (value) =&gt; {

// Urgente: atualiza o input imediatamente

setInputValue(value);

// Não urgente: calcula e renderiza resultados em background

startTransition(() =&gt; {

// Simulando busca pesada (em produção, seria uma API call)

const filtered = simulateExpensiveSearch(value);

setResults(filtered);

});

};

return (

&lt;div&gt;

&lt;input

value={inputValue}

onChange={(e) =&gt; handleSearch(e.target.value)}

placeholder=&quot;Buscar...&quot;

/&gt;

{isPending &amp;&amp; &lt;div className=&quot;spinner&quot;&gt;Carregando resultados...&lt;/div&gt;}

&lt;ul className={isPending ? &#039;opacity-50&#039; : &#039;&#039;}&gt;

{results.map(result =&gt; (

&lt;li key={result.id}&gt;{result.title}&lt;/li&gt;

))}

&lt;/ul&gt;

&lt;/div&gt;

);

}

function simulateExpensiveSearch(query) {

// Simulação de operação pesada

const start = performance.now();

while (performance.now() - start &lt; 500) {}

return Array.from({ length: 50 }, (_, i) =&gt; ({

id: i,

title: Resultado: ${query} - ${i}

}));

}</code></pre>

<h2>useOptimistic: Feedback Imediato com Sincronização Segura</h2>

<h3>Quando Você Precisa Adivinhar o Futuro</h3>

<p><code>useOptimistic</code> resolve um problema diferente: quando você envia uma ação para o servidor (POST, PUT, DELETE), o usuário quer <strong>ver o resultado imediatamente</strong>, 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.</p>

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

<h3>Exemplo Prático: Like Button</h3>

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

<pre><code class="language-jsx">import { useOptimistic, useState } from &#039;react&#039;;

export function PostWithLike() {

const [post, setPost] = useState({

id: 1,

title: &#039;Meu Primeiro Post&#039;,

likes: 42,

liked: false

});

const [optimisticPost, addOptimisticLike] = useOptimistic(

post,

(currentPost, newLikes) =&gt; ({

...currentPost,

likes: newLikes,

liked: !currentPost.liked

})

);

const handleLike = async () =&gt; {

// 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: &#039;POST&#039;

});

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(&#039;Erro ao fazer like:&#039;, error);

}

};

return (

&lt;div className=&quot;post&quot;&gt;

&lt;h2&gt;{optimisticPost.title}&lt;/h2&gt;

&lt;button

onClick={handleLike}

className={optimisticPost.liked ? &#039;liked&#039; : &#039;&#039;}

&gt;

❤️ {optimisticPost.likes}

&lt;/button&gt;

&lt;/div&gt;

);

}</code></pre>

<p>Veja como funciona:</p>

<ol>

<li>Você clica no botão</li>

<li>O estado muda <strong>imediatamente</strong> — o contador aumenta e a cor muda</li>

<li>Em paralelo, a requisição HTTP é enviada</li>

<li>Se a resposta vier com sucesso, <code>setPost</code> sincroniza o estado real</li>

<li>Se falhar, o React <strong>automaticamente</strong> reverte para o estado anterior sem você fazer nada</li>

</ol>

<h3>Padrão Avançado: Múltiplas Ações Otimistas</h3>

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

<pre><code class="language-jsx">import { useOptimistic, useState } from &#039;react&#039;;

export function TodoList() {

const [todos, setTodos] = useState([

{ id: 1, title: &#039;Aprender React 18&#039;, completed: false }

]);

const [optimisticTodos, addOptimisticTodo] = useOptimistic(

todos,

(state, newTodo) =&gt; [...state, newTodo]

);

const handleAddTodo = async (title) =&gt; {

const tempTodo = {

id: Date.now(),

title,

completed: false

};

// Mostra o novo todo imediatamente

addOptimisticTodo(tempTodo);

try {

const response = await fetch(&#039;/api/todos&#039;, {

method: &#039;POST&#039;,

headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },

body: JSON.stringify({ title })

});

const createdTodo = await response.json();

// Substitui o otimista pelo real (com ID real do servidor)

setTodos(current =&gt;

current.map(t =&gt; t.id === tempTodo.id ? createdTodo : t)

);

} catch (error) {

console.error(&#039;Erro ao criar todo:&#039;, error);

// Automaticamente reverte

}

};

return (

&lt;div&gt;

&lt;button onClick={() =&gt; handleAddTodo(&#039;Novo Todo&#039;)}&gt;

Adicionar

&lt;/button&gt;

&lt;ul&gt;

{optimisticTodos.map(todo =&gt; (

&lt;li key={todo.id} className={todo.completed ? &#039;completed&#039; : &#039;&#039;}&gt;

{todo.title}

&lt;/li&gt;

))}

&lt;/ul&gt;

&lt;/div&gt;

);

}</code></pre>

<h2>Combinando Ambos os Hooks: O Padrão Completo</h2>

<h3>Quando Usar Cada Um</h3>

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

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

<h3>Exemplo Prático: Formulário de Reserva</h3>

<pre><code class="language-jsx">import { useState, useTransition, useOptimistic } from &#039;react&#039;;

export function BookingForm() {

const [booking, setBooking] = useState({

date: &#039;&#039;,

time: &#039;&#039;,

guests: 1,

status: &#039;idle&#039; // idle, submitting, success, error

});

const [optimisticBooking, addOptimisticBooking] = useOptimistic(

booking,

(current, updates) =&gt; ({ ...current, ...updates })

);

const [isPending, startTransition] = useTransition();

const handleSubmit = async (e) =&gt; {

e.preventDefault();

// Atualiza otimisticamente com status de submissão

addOptimisticBooking({ status: &#039;submitting&#039; });

// Processamento em background (pode ser pesado)

startTransition(async () =&gt; {

try {

const response = await fetch(&#039;/api/bookings&#039;, {

method: &#039;POST&#039;,

headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },

body: JSON.stringify(booking)

});

if (response.ok) {

const result = await response.json();

setBooking(prev =&gt; ({

...prev,

status: &#039;success&#039;,

...result

}));

} else {

setBooking(prev =&gt; ({

...prev,

status: &#039;error&#039;

}));

}

} catch (error) {

setBooking(prev =&gt; ({

...prev,

status: &#039;error&#039;

}));

}

});

};

const handleChange = (e) =&gt; {

const { name, value } = e.target;

setBooking(prev =&gt; ({

...prev,

[name]: value

}));

};

return (

&lt;form onSubmit={handleSubmit}&gt;

&lt;input

type=&quot;date&quot;

name=&quot;date&quot;

value={optimisticBooking.date}

onChange={handleChange}

disabled={optimisticBooking.status === &#039;submitting&#039;}

/&gt;

&lt;input

type=&quot;time&quot;

name=&quot;time&quot;

value={optimisticBooking.time}

onChange={handleChange}

disabled={optimisticBooking.status === &#039;submitting&#039;}

/&gt;

&lt;input

type=&quot;number&quot;

name=&quot;guests&quot;

min=&quot;1&quot;

max=&quot;10&quot;

value={optimisticBooking.guests}

onChange={handleChange}

disabled={optimisticBooking.status === &#039;submitting&#039;}

/&gt;

&lt;button type=&quot;submit&quot; disabled={optimisticBooking.status === &#039;submitting&#039;}&gt;

{optimisticBooking.status === &#039;submitting&#039; ? &#039;Reservando...&#039; : &#039;Reservar&#039;}

&lt;/button&gt;

{optimisticBooking.status === &#039;success&#039; &amp;&amp; (

&lt;p className=&quot;success&quot;&gt;Reserva confirmada!&lt;/p&gt;

)}

{optimisticBooking.status === &#039;error&#039; &amp;&amp; (

&lt;p className=&quot;error&quot;&gt;Erro ao fazer reserva. Tente novamente.&lt;/p&gt;

)}

{isPending &amp;&amp; (

&lt;p className=&quot;info&quot;&gt;Processando reserva...&lt;/p&gt;

)}

&lt;/form&gt;

);

}</code></pre>

<p>Neste exemplo:</p>

<ol>

<li>O usuário submete o formulário</li>

<li>Imediatamente, o estado é atualizado otimisticamente com <code>status: &#039;submitting&#039;</code></li>

<li>O botão desabilita e mostra &quot;Reservando...&quot;</li>

<li>A requisição HTTP é enviada dentro de <code>startTransition</code></li>

<li>Se suceder, <code>setBooking</code> atualiza com a resposta real</li>

<li>Se falhar, o estado reverte e mostra erro</li>

</ol>

<h2>Otimizações Práticas e Armadilhas Comuns</h2>

<h3>O Perigo do Uso Incorreto</h3>

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

<pre><code class="language-jsx"></code></pre>

<h3>Medindo o Impacto Real</h3>

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

<pre><code class="language-jsx">export function PerformanceMonitor({ children }) {

return (

&lt;&gt;

&lt;React.Profiler

id=&quot;main&quot;

onRender={(id, phase, actualDuration) =&gt; {

if (actualDuration &gt; 16) { // ~60fps threshold

console.warn(

Render lento em ${id}: ${actualDuration.toFixed(2)}ms

);

}

}}

&gt;

{children}

&lt;/React.Profiler&gt;

&lt;/&gt;

);

}</code></pre>

<h3>Compatibilidade com Server Components (React 19)</h3>

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

<pre><code class="language-jsx">&#039;use client&#039;;

import { useOptimistic } from &#039;react&#039;;

export function ClientComponent({ updateServerData }) {

const [optimisticData, addOptimisticUpdate] = useOptimistic(

null,

(_, newValue) =&gt; newValue

);

const handleClick = async () =&gt; {

addOptimisticUpdate(&#039;Loading...&#039;);

await updateServerData();

};

return (

&lt;button onClick={handleClick}&gt;

{optimisticData || &#039;Click&#039;}

&lt;/button&gt;

);

}</code></pre>

<h2>Conclusão</h2>

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

<p>O aprendizado prático mais importante é <strong>simplicidade</strong>: 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 <code>useTransition</code> bem colocado pode transformar uma interface que parecia ruim em excelente, com apenas 3 linhas de código.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://react.dev/reference/react/useTransition" target="_blank" rel="noopener noreferrer">React 18 Hooks Documentation - useTransition</a></li>

<li><a href="https://react.dev/reference/react/useOptimistic" target="_blank" rel="noopener noreferrer">React 18 Hooks Documentation - useOptimistic</a></li>

<li><a href="https://react.dev/blog/2022/03/29/react-v18" target="_blank" rel="noopener noreferrer">React 18 Concurrent Features Guide</a></li>

<li><a href="https://web.dev/vitals/" target="_blank" rel="noopener noreferrer">Web Vitals: Core Web Vitals Explained</a></li>

<li><a href="https://react.dev/learn/render-and-commit" target="_blank" rel="noopener noreferrer">React Performance Optimization Patterns</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

O que Todo Dev Deve Saber sobre Design System com React: Tokens, Variantes e Acessibilidade
O que Todo Dev Deve Saber sobre Design System com React: Tokens, Variantes e Acessibilidade

O Que é um Design System e Por Que Usá-lo Um Design System é um conjunto de c...

O que Todo Dev Deve Saber sobre useRef Avançado: DOM, Valores Mutáveis e Comunicação entre Renders
O que Todo Dev Deve Saber sobre useRef Avançado: DOM, Valores Mutáveis e Comunicação entre Renders

O que é useRef e por que vai além de useState O é um hook do React que retorn...

Code Splitting em React: lazy, Suspense e Dynamic Imports na Prática
Code Splitting em React: lazy, Suspense e Dynamic Imports na Prática

O que é Code Splitting em React Code splitting é uma estratégia de otimização...