Assinar
Voltar
Frontend

O use(browser()) do React 19.3 não é o seu mounted com nome melhor. Sem Suspense, o servidor falha

O React 19.3 transformou a gambiarra do mounted numa API de primeira classe. Ela precisa de um limite de Suspense acima, senão o render do servidor falha, e os casos não aparecem no seu onError.

</>

O React 19.3 saiu em 9 de setembro de 2026. A manchete ficou com as View Transitions e os Fragment Refs, que estabilizaram depois de um ano como experimentais. Mas a novidade que mais gente vai usar sem perceber está no react-dom, e chama browser.

Todo projeto com renderização no servidor tem aquele componente que não roda no servidor. Ele lê localStorage, ou o fuso horário do aparelho, ou qualquer API que só existe no navegador. E toda base de código tem a mesma gambiarra para lidar com ele:

function Component() {
  const [mounted, setMounted] = useState(false);
  useEffect(() => {
    setMounted(true)
  }, [])
  // ...
}

O React 19.3 transformou isso numa API de primeira classe. A posição deste post: chamar o use(browser()) de “o mounted com nome melhor” é o caminho mais rápido para quebrar o seu servidor em produção. Ele não degrada, ele falha. E o mecanismo dele é outro.

Como se escreve

São duas importações e uma linha:

import { use } from 'react';
import { browser } from 'react-dom';

function Component() {
  use(browser('Este componente depende de API do navegador.'));
  // ...
}

Durante a renderização no servidor, o use(browser()) para a renderização do componente e deixa o fallback do <Suspense> mais próximo no lugar dele. No navegador, ele devolve undefined, e o componente renderiza normalmente.

O argumento é opcional e é uma razão, string ou função, que explica por que aquele conteúdo precisa do navegador. Ela vira a cause do erro entregue ao onBrowserBailout, que veremos adiante. A função só é chamada no servidor, nunca no navegador, então se montar a razão for caro, passe função em vez de string.

A caveat que muda tudo

A documentação de referência lista três restrições. A primeira é a que decide se você adota isso hoje ou depois de mexer na sua árvore:

O use(browser()) precisa estar dentro de um limite de <Suspense> durante a renderização no servidor. Sem um, o render do servidor falha.

Compare com a gambiarra que ele substitui. O mounted em estado não exige nada da árvore em volta. Se não houver Suspense por perto, o componente simplesmente devolve null no servidor e ninguém fica sabendo. É feio e funciona em qualquer lugar.

O use(browser()) não tem esse comportamento de escape. Sem limite, o render inteiro do servidor falha, e o React reporta a falha pelos callbacks de erro habituais do renderizador, não pelo onBrowserBailout.

Isso não é defeito, é desenho: a API troca conteúdo por fallback, e fallback só existe dentro de um Suspense. Mas significa que trocar o mounted pelo use(browser()) é uma refatoração de duas partes, a chamada e o limite. Quem fizer só a primeira, num componente que hoje devolve null discretamente, troca degradação silenciosa por erro de servidor.

As outras duas restrições

Em aplicação com React Server Components, o use(browser()) precisa ser chamado de um Client Component. Se o seu framework usa Server Components por padrão, isso quer dizer a diretiva 'use client' no arquivo, ou mover a chamada para um Client Component filho.

E chamar browser() sozinho não faz nada. É o valor devolvido por ele, passado ao use, que marca o componente. Não jogue esse valor com throw.

Por que o resultado é melhor que o do mounted

A diferença aparece no HTML que o usuário recebe.

Com o mounted, o servidor renderiza o ramo falso: null, ou um esqueleto que você escreveu à mão. O HTML sai com esse buraco. No cliente, a primeira renderização também cai no ramo falso, porque o efeito ainda não rodou. Só depois da hidratação o efeito dispara, o estado muda, e vem uma segunda renderização com o conteúdo de verdade. São dois passos no cliente, e o buraco não participa de nada.

Com o use(browser()), o servidor deixa no HTML o fallback do Suspense mais próximo, o mesmo usado por qualquer outro conteúdo suspenso daquele limite. É a diferença prática: o seu componente de navegador entra na mesma sequência de carregamento dos componentes que suspendem por dados, em vez de ter um estado de carregamento paralelo que você mantém à mão.

No cliente, o use(browser()) não suspende. Depois da hidratação o componente renderiza normal, sem o passo extra de efeito.

Chamada condicional, que é o detalhe bonito

Como qualquer chamada ao use, o use(browser()) pode aparecer dentro de um if ou depois de um return antecipado. Isso permite um Hook decidir se abre mão do servidor com base num argumento:

export function useTimeZone(defaultTimeZone) {
  if (defaultTimeZone !== undefined) {
    return defaultTimeZone;
  }
  use(browser('Nenhum fuso padrão foi informado.'));
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
}

Com valor padrão, o React renderiza o conteúdo no HTML inicial. Sem valor padrão, o componente suspende no servidor e mostra o fuso do aparelho no navegador. O mesmo Hook, dois comportamentos, decididos por prop.

O padrão equivalente para busca de dados fecha o argumento: um useBrowserQuery que chama use(browser()) apenas quando não recebeu initialData. Com dados iniciais vindos de um Server Component ou do loader do framework, o componente entra no HTML. Sem eles, espera o navegador.

O seu monitoramento não vai ver isso

Falta um detalhe operacional, e ele é fácil de descobrir tarde demais.

Quando o React deixa um fallback de Suspense para o navegador por causa do browser, ele não chama o onError do renderizador de servidor nem o onRecoverableError do hydrateRoot. Existe um callback novo e específico para isso, o onBrowserBailout:

const { pipe } = renderToPipeableStream(<App />, {
  onShellReady() {
    pipe(response);
  },
  onBrowserBailout(error, errorInfo) {
    logBrowserBailout(error, errorInfo);
  }
});

Ele recebe um Error descrevendo a renderização adiada, com a sua razão disponível em cause, e um errorInfo com componentStack mostrando onde aconteceu. A razão não é serializada no HTML, então ela não vaza para o cliente.

A consequência prática: se você adotar o use(browser()) sem ligar esse callback, a quantidade de conteúdo que o seu servidor deixou de renderizar fica invisível. Nada aparece nos seus contadores de erro, e não é para aparecer, porque não é erro. Só que sem instrumento você também não descobre quando um componente inocente foi parar dentro de um ramo que suspende e levou meia página junto.

O mesmo valor serve de motivo para abortar um render pendente e entregar o resto ao navegador, passando browser('O render do servidor estourou o tempo.') como razão do abort. Cada limite recuperado assim também é reportado ao onBrowserBailout.

Perguntas frequentes

Posso trocar todos os meus mounted por use(browser())?

Só depois de garantir um <Suspense> acima de cada um. Sem o limite, o render do servidor falha, enquanto o mounted apenas devolvia null.

Ele evita erro de hidratação?

Ele evita a causa mais comum, que é servidor e cliente renderizando saídas diferentes para o mesmo componente. No servidor fica o fallback, e o conteúdo real só existe no navegador.

Funciona em Server Component?

Não. Em aplicação com React Server Components a chamada precisa vir de um Client Component, o que na prática significa a diretiva 'use client' no arquivo ou mover a chamada para um filho cliente.

O use(browser()) pode ficar dentro de um if?

Pode. Como as outras chamadas ao use, ele aceita condicional e return antecipado, e é isso que permite um Hook abrir mão do servidor conforme o valor de uma prop.

Preciso atualizar o monitoramento?

Sim, se quiser enxergar o efeito. Esses casos não passam pelo onError nem pelo onRecoverableError, e só chegam no onBrowserBailout.

Fontes

  • React 19.3, blog do React, 9 de setembro de 2026
  • browser, referência da API do react-dom, onde constam as caveats e o onBrowserBailout

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *