Introdução ao Concurrent Mode
O Concurrent Mode é uma das features mais transformadoras introduzidas pelo React nos últimos anos. Diferentemente do modo tradicional, onde o React renderiza componentes de forma síncrona e bloqueia a thread principal, o Concurrent Mode permite que o React interrompa, pause e retome renderizações. Isso significa que operações de longa duração não congelam a interface do usuário, mantendo a responsividade mesmo em aplicações complexas.
Para entender a importância disso, imagine um formulário com validação pesada ou uma lista com milhares de itens. No React tradicional, a renderização dessa lista poderia travar a interface por segundos. Com Concurrent Mode, o React pode pausar essa renderização, processar cliques do usuário ou outras prioridades, e depois retomar. Isso não é magia — é uma reimplementação do algoritmo de reconciliação do React, construído sobre um scheduler mais sofisticado.
Suspense: Manipulando Carregamento Assincronamente
O que é Suspense e como funciona
Suspense é um componente que permite que você especifique o que mostrar enquanto os dados ainda estão sendo carregados. Funciona capturando uma Promise lançada durante a renderização e pausando aquele componente até que a Promise seja resolvida. Quando a Promise resolve, o React retoma a renderização com os dados disponíveis.
A ideia central é simples: em vez de usar useEffect com estados loading e error, o componente lança uma Promise durante a renderização. O Suspense acima na árvore captura essa Promise e mostra um fallback até que ela resolva. Isso inverte o fluxo tradicional e torna o código muito mais limpo.
// Exemplo simples: um componente que lança uma Promise
const fetchUser = (id) => {
let status = 'pending';
let result;
const promise = fetch(`/api/users/${id}`)
.then(res => res.json())
.then(data => {
status = 'success';
result = data;
})
.catch(err => {
status = 'error';
result = err;
});
return {
read() {
if (status === 'pending') throw promise;
if (status === 'error') throw result;
return result;
}
};
};
const userResource = fetchUser(1);
function UserProfile() {
const user = userResource.read();
return <div>{user.name}</div>;
}
export default function App() {
return (
<Suspense fallback={<div>Carregando usuário...</div>}>
<UserProfile />
</Suspense>
);
}
Casos de uso práticos
Suspense brilha em cenários onde você precisa carregar dados antes de renderizar um componente. Code splitting é o caso mais comum: você quer carregar um componente dinamicamente, e enquanto o bundle não chega, mostra um loading. Suspense também é excelente para orquestrar múltiplas requisições de dados, evitando o "loading em cascata" onde cada componente carrega seus dados independentemente.
Na prática, você raramente criará o padrão de resource como mostrei acima. Bibliotecas como React Query e SWR já integram com Suspense nativamente. Veja como fica com React Query:
import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
suspense: true,
});
return <div>{user.name}</div>;
}
export default function App() {
return (
<Suspense fallback={<div>Carregando...</div>}>
<UserProfile userId={1} />
</Suspense>
);
}
Transitions: Mantendo a Interface Responsiva
Entendendo transições e prioridades
Nem todas as atualizações de estado são iguais. Quando o usuário digita em um input, essa atualização é urgente — o React deve processar imediatamente para manter a sensação de responsividade. Mas quando essa digitação desencadeia uma busca em uma lista filtrada, essa busca é menos urgente. Se o React dedicar todo seu tempo à busca, o input pode ficar travado.
Transitions permitem marcar certos updates como menos urgentes, dando ao React permissão para interrompê-los se houver algo mais importante. A API é simples: use useTransition e envolva o setState com startTransition.
import { useTransition, useState } from 'react';
function SearchUsers() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
// Update do input é urgente
setQuery(value);
// Busca é menos urgente
startTransition(() => {
const filtered = heavyFilter(value);
setResults(filtered);
});
};
return (
<div>
<input
type="text"
value={query}
onChange={handleChange}
placeholder="Digite para buscar..."
/>
{isPending && <span>Buscando...</span>}
<ul>
{results.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
function heavyFilter(query) {
// Simula uma operação pesada
const mockUsers = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `User ${i}`
}));
return mockUsers.filter(u =>
u.name.toLowerCase().includes(query.toLowerCase())
);
}
export default SearchUsers;
Navegação com transições
Um caso de uso extremamente comum é a navegação. Quando o usuário clica em um link, você quer mudar a rota imediatamente (para feedback visual), mas carregar o novo conteúdo pode ser pesado. Transições lidam com isso perfeitamente:
import { useTransition } from 'react';
import { useNavigate } from 'react-router-dom';
function Navigation() {
const navigate = useNavigate();
const [isPending, startTransition] = useTransition();
const handleNavigation = (path) => {
startTransition(() => {
navigate(path);
});
};
return (
<nav>
<button
onClick={() => handleNavigation('/home')}
disabled={isPending}
>
Home {isPending && '(carregando...)'}
</button>
<button
onClick={() => handleNavigation('/about')}
disabled={isPending}
>
Sobre {isPending && '(carregando...)'}
</button>
</nav>
);
}
export default Navigation;
O isPending retornado por useTransition indica se há uma transição em andamento. Use isso para desabilitar botões, mostrar spinners ou manter o conteúdo anterior na tela enquanto o novo está sendo preparado.
useDeferredValue: Adiando Computações Custosas
Quando adiar valores é mais simples que transições
Enquanto useTransition marca uma ação como menos urgente, useDeferredValue marca um valor como menos urgente. A diferença é sutil mas importante: você não controla quando a atualização acontece. O React recebe um novo valor, sabe que pode ser desatualizado, e processa quando tiver tempo.
Isso é perfeito para situações onde você recebe um novo valor (como um prop filtrado ou estado) e quer renderizar com ele, mas não quer bloquear a interface se a renderização for cara. Diferentemente de useTransition, você não precisa envolver nada em uma função callback.
import { useDeferredValue, useState } from 'react';
function ListComponent({ items }) {
// deferredItems começará com items, mas será atualizado com baixa prioridade
const deferredItems = useDeferredValue(items);
return (
<ul>
{deferredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
export default function App() {
const [query, setQuery] = useState('');
// Filtra imediatamente para manter o input responsivo
const filteredItems = items.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Digite para filtrar..."
/>
<ListComponent items={filteredItems} />
</div>
);
}
const items = Array.from({ length: 5000 }, (_, i) => ({
id: i,
name: `Item ${i}`
}));
Diferença prática entre useTransition e useDeferredValue
A confusão é comum. useTransition é para ações — você sabe quando algo pesado vai acontecer e marca com startTransition. useDeferredValue é para valores — você recebe um novo valor e quer renderizar com baixa prioridade. Se você controla o setState, use useTransition. Se você recebe um valor como prop e quer deferir a renderização, use useDeferredValue.
// useTransition: você controla quando a operação pesada começa
function Example1() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleSearch = (value) => {
setQuery(value);
startTransition(() => {
setResults(expensiveSearch(value));
});
};
return (
<div>
<input onChange={(e) => handleSearch(e.target.value)} />
{isPending ? 'Buscando...' : `${results.length} resultados`}
</div>
);
}
// useDeferredValue: o valor vem de um prop ou cálculo externo
function Example2({ query }) {
const deferredQuery = useDeferredValue(query);
const results = expensiveSearch(deferredQuery);
return (
<div>
{query !== deferredQuery && 'Atualizando...'}
{results.length} resultados
</div>
);
}
Note que em Example2, você pode comparar query (atual) com deferredQuery (atrasado) para saber se está desatualizado. Isso é útil para mostrar indicadores visuais.
Padrões Avançados e Boas Práticas
Combinando Suspense, Transitions e useDeferredValue
As três features trabalham juntas perfeitamente. Aqui está um exemplo completo e realista:
import {
Suspense,
useTransition,
useDeferredValue,
useState
} from 'react';
// Componente que lança uma Promise (simula fetch)
function SearchResults({ query }) {
const [results] = useState(() => {
// Simula uma requisição que demora
throw new Promise(resolve => {
setTimeout(() => {
resolve([
{ id: 1, title: `Resultado para "${query}"` },
{ id: 2, title: `Outro resultado para "${query}"` }
]);
}, 1000);
});
});
return (
<div>
{results.map(r => (
<div key={r.id}>{r.title}</div>
))}
</div>
);
}
function SearchApp() {
const [input, setInput] = useState('');
const [isPending, startTransition] = useTransition();
const deferredInput = useDeferredValue(input);
const handleChange = (e) => {
const value = e.target.value;
setInput(value); // Urgente
startTransition(() => {
// O setState aqui marca a busca como menos urgente
// O useDeferredValue garante que renderizamos com o valor atrasado
});
};
return (
<div>
<input
value={input}
onChange={handleChange}
placeholder="Busque algo..."
/>
<Suspense fallback={<div>Carregando resultados...</div>}>
{deferredInput && <SearchResults query={deferredInput} />}
</Suspense>
{isPending && <p>Atualizando resultados...</p>}
</div>
);
}
export default SearchApp;
Evitando armadilhas comuns
A maior armadilha é tentar usar Concurrent Mode com código antigo que não foi preparado para isso. Se você tem useEffect lançando requests sem proper cleanup, Suspense vai quebrar. Certifique-se de usar bibliotecas compatíveis como React Query, SWR, ou Relay.
Outra armadilha é não entender que useDeferredValue retorna um valor potencialmente desatualizado. Se você atualiza um filtro e immediamente renderiza baseado no deferredValue, o usuário verá a lista antiga por um breve momento. Isso é proposital — Trade-off entre responsividade e latência. Use indicadores visuais para informar ao usuário que está desatualizado.
function GoodExample() {
const [input, setInput] = useState('');
const deferredInput = useDeferredValue(input);
const isStale = input !== deferredInput;
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
/>
{isStale && <div style={{ opacity: 0.5 }}>Aguardando atualização...</div>}
<Results query={deferredInput} />
</div>
);
}
Conclusão
Você aprendeu que Concurrent Mode não é um único recurso, mas um paradigma onde o React pode interromper e retomar renderizações, priorizando interações de usuário. Suspense simplifica o carregamento de dados inverting o fluxo para lançar Promises, useTransition permite marcar updates como menos urgentes, e useDeferredValue adia a renderização de valores para manter responsividade. A chave é usar a ferramenta correta para o problema: Suspense para dados, Transitions para ações, e useDeferredValue para valores computados custosos. Comece simples, implemente com bibliotecas maduras como React Query, e observe como seu aplicativo fica genuinamente mais responsivo — não por truques visuais, mas por priorização real de trabalho.