Menu

Hook useTransition no React: mantenha a UI responsiva

useTransition marca uma atualização de estado como não urgente, para que o React mantenha a página responsiva enquanto renderiza a parte lenta em segundo plano. Aprenda isPending, startTransition, transições assíncronas no React 19 e por que isso não é debounce.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

useTransition permite marcar uma atualização de estado como não urgente. O React renderiza essa atualização em segundo plano e a descarta ou recomeça se acontecer algo mais urgente, como um clique ou uma tecla, então a página continua responsiva enquanto uma parte lenta da tela se atualiza.

Clique em posts e, antes de a lista aparecer, clique em contact. O contact aparece na hora: o React descartou a renderização dos posts pela metade. Agora troque startTransition(() => { setTab(next); }) por um simples setTab(next); e tente a mesma coisa: a página congela por cerca de 300 ms depois do clique em posts, e o seu clique em contact espera os posts terminarem.

A sintaxe

const [isPending, startTransition] = useTransition();

startTransition(() => {
    setTab(next);
});
  • startTransition recebe uma função e a executa na hora. Todas as atualizações de estado dentro dela são marcadas como transições.
  • isPending é true desde o momento em que você inicia a transição até o React terminar de renderizá-la e aplicar o resultado.

O useTransition não recebe argumentos. A função que você passa executa de forma síncrona; a transição diz respeito a como o React renderiza a atualização resultante, não a quando a sua função executa.

O que significa "não urgente"

O React divide as atualizações em dois tipos. Atualizações urgentes, como digitar, clicar e pressionar, precisam aparecer na hora ou a página parece quebrada. Atualizações de transição, como trocar para uma nova visualização ou mostrar resultados filtrados, podem levar um momento.

O React renderiza uma transição em pequenas fatias e confere entre os componentes se chegou algo urgente. Se chegou, o React pausa ou abandona a transição, trata a atualização urgente e então recomeça a transição com o estado mais recente. Enquanto uma transição renderiza, a tela anterior continua visível; o React não mostra um resultado montado pela metade. É por isso que o exemplo continua mostrando o texto do about, com a dica Loading ao lado dos botões, até os posts ficarem prontos.

A interrupção acontece entre componentes. Um único componente que bloqueia por 300 ms não pode ser cortado ao meio, e é por isso que o trabalho lento acima está espalhado por 300 pequenos componentes SlowPost.

Mostrando uma dica de pendência

O isPending é o lugar para um indicador de carregamento que não substitui o conteúdo atual: um spinner ao lado do botão, um texto esmaecido, um rótulo "Saving...". No primeiro exemplo ele mostra Loading... ao lado das abas. Você também pode usá-lo para estilizar o conteúdo:

<div style={{ opacity: isPending ? 0.6 : 1 }}>{content}</div>

Filtrando uma lista grande enquanto digita

Você não pode colocar o próprio valor de um input em uma transição. Um <input> controlado precisa receber o novo valor na mesma atualização urgente, ou caracteres se perdem. Mantenha duas partes de estado: o texto do input (urgente) e a busca que a lista usa (uma transição).

Digite 123 rápido. O input nunca trava. O Console ainda registra uma linha rendering results por tecla, porque Results registra assim que o React começa a renderizá-lo, mas as listas para 1 e 12 nunca chegam à tela: cada tecla mais nova interrompeu a renderização em andamento, e o React recomeçou com a busca mais recente. Results está envolvido em memo de propósito: sem isso, a renderização urgente de cada tecla também renderizaria a lista lenta com a busca antiga, e você perderia o benefício.

startTransition sem o hook

O react também exporta o startTransition como uma função comum. Ele marca as atualizações como transições do mesmo jeito que o hook, mas você não recebe o isPending. Use-o onde você não pode chamar um hook, por exemplo em uma biblioteca de dados ou em um roteador fora de qualquer componente.

import { startTransition } from 'react';

function navigate(url) {
    startTransition(() => {
        setCurrentUrl(url);
    });
}

Transições assíncronas no React 19

Desde o React 19 a função que você passa pode ser async. O isPending então fica true durante todo o tempo em que a função executa, incluindo a espera, o que é um jeito simples de mostrar um estado pendente durante uma requisição. O saveName falso abaixo faz o papel de uma requisição real.

Repare no segundo startTransition em volta de setSaved. O React perde o rastro da transição através de um await, então uma atualização de estado depois dele precisa ser envolvida de novo para contar como parte da transição. O React chama uma função passada assim de action; o useActionState e as form actions se apoiam no mesmo mecanismo.

Uma transição não é um debounce

Um debounce espera um tempo fixo depois da última tecla antes de fazer qualquer coisa. Uma transição não espera. Ela começa a renderizar na hora e, se chega uma atualização mais nova, descarta o trabalho antigo. Em um aparelho rápido o resultado aparece sem atraso nenhum; em um lento, o React faz o máximo que consegue entre as teclas. O que uma transição não consegue é reduzir o número de requisições de rede: se cada tecla inicia um fetch, faça o debounce do fetch.

Transições e Suspense

As transições também mudam o que um boundary de Suspense faz. Normalmente, quando um componente dentro de um boundary suspende (um componente lazy carregando, ou dados lidos com use), o React substitui o conteúdo pelo fallback. Se a atualização que causou a suspensão foi uma transição, o React mantém o conteúdo que já está na tela, e o isPending fica true até o novo conteúdo ficar pronto. É assim que os roteadores evitam piscar um spinner do tamanho da página a cada navegação: eles envolvem a navegação em uma transição, e a página antiga continua visível até a próxima poder renderizar. Conteúdo que nunca foi mostrado continua usando o fallback, então um boundary recém-revelado se comporta como de costume.

O que não funciona

  • Atualizações dentro de um timeout. startTransition(() => setTimeout(() => setTab(next), 100)) não marca a atualização, porque ela executa depois que o startTransition já retornou. Coloque o startTransition dentro do timeout.
  • Um único componente lento. O React só consegue interromper entre componentes, então um componente que bloqueia por muito tempo continua bloqueando.
  • Controlar um input. Como acima, o próprio estado do input continua urgente.

Para o caso em que você não é dono do setter de estado e só recebe um valor, como uma prop, o useDeferredValue dá a você o mesmo comportamento.

Perguntas frequentes

O que o useTransition faz?

Ele dá a você o startTransition, que marca as atualizações de estado dentro dele como não urgentes, e o isPending, que é true enquanto essa atualização ainda está renderizando. O React pode interromper uma renderização não urgente para tratar primeiro cliques e digitação.

Qual é a diferença entre useTransition e startTransition?

Os dois marcam atualizações como transições. O hook useTransition também dá a você o isPending para uma dica de carregamento. O startTransition avulso importado de react funciona fora de componentes, mas não tem indicador de pendência.

Posso usar o useTransition para um input de texto?

Não para o próprio valor do input. Um input controlado precisa atualizar de forma síncrona, ou a digitação quebra. Mantenha o estado do input urgente e coloque em uma transição a atualização cara que depende dele, ou use o useDeferredValue.

O useTransition é o mesmo que debounce?

Não. Um debounce espera um tempo fixo antes de fazer qualquer coisa. Uma transição começa a renderizar na hora e é interrompida se chega uma atualização mais nova, então em um aparelho rápido o resultado aparece sem atraso nenhum.

O startTransition pode receber uma função async?

Sim, desde o React 19. O isPending fica true até a função assíncrona terminar. Atualizações de estado depois de um await precisam ser envolvidas em outra chamada de startTransition para serem marcadas como transição.

Ilustração das linguagens de programação do Coddy

Aprenda a programar com o Coddy

COMEÇAR