Guia Completo de Concurrent Mode em React: Suspense, Transitions e useDeferredValue Já leu

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

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.

Referências


Artigos relacionados