use(browser()): o React 19.3 aposentou o typeof window
O React 19.3 saiu com uma API pequena que me chamou mais atenção do que o resto: use(browser()), um jeito oficial de dizer que um componente só existe no browser. Neste post, mostro o problema que ela resolve, os jeitos que a gente contornava isso até agora e por que ela não resolve tudo o que parece.
4 min de leitura
Nesta página
O React 19.3 saiu no dia 9 de setembro e, lendo a release, a feature que mais me chamou atenção foi a menor delas: o use(browser()). É um jeito oficial de dizer ao React que um componente só existe no browser.
Parece pouca coisa, mas é o fim de um hack que quase todo projeto com SSR tem escondido em algum lugar: o velho typeof window !== 'undefined'.
Antes de chegar na API nova, vale entender o problema que ela tenta resolver.
O que é um hydration mismatch
Com SSR, o servidor gera o HTML da página e manda para o browser. O browser mostra esse HTML na hora, monta a árvore de componentes de novo e compara com o que chegou. Esse processo é a hidratação.
Se a primeira renderização no cliente sair diferente da do servidor, o React reclama. E tem componente que sai diferente por definição. O exemplo clássico é um toggle de tema que lê a preferência salva no localStorage: no servidor não existe localStorage, então ele não tem como saber que você prefere o tema escuro.
O typeof window faz exatamente o que o React não quer: dá false lá e true aqui. O servidor manda "claro", o browser renderiza "escuro" e pronto, mismatch.
Dá play aí embaixo para ver isso acontecendo, e depois compara com as outras abordagens:
No servidor não existe window, então o componente cai no padrão: tema claro.
Os três jeitos de contornar (até agora)
Até o 19.3, isso se resolvia de três jeitos, nenhum muito bonito.
O mounted no useEffect
É o que eu mais vejo por aí. Um estado mounted que começa false e vira true num useEffect:
Funciona, mas repara: é um efeito sem efeito nenhum. Ele só existe para marcar que a hidratação passou. E como o componente não renderiza nada até lá, o que vem depois dele na página pula quando ele aparece.
dynamic com ssr: false
No Next.js dá para resolver por fora do componente, pedindo para ele nem ser renderizado no servidor:
O componente fica limpo, mas a decisão de "isso só roda no browser" passa para quem importa o componente. E se você esquecer a opção loading, o espaço fica vazio no HTML do mesmo jeito.
useSyncExternalStore com getServerSnapshot
Esse é considerado o jeito certo. O useSyncExternalStore aceita um terceiro argumento, o getServerSnapshot, que é usado no servidor e durante a hidratação:
Isso acaba com o mismatch e com o efeito falso. Só que, logo depois da hidratação, o React lê o valor de verdade, percebe que mudou e força um update síncrono com re-render. Na prática, quem prefere o tema escuro vê o claro piscar antes. Muita cerimônia para comunicar uma coisa só. 😒
Chega o use(browser())
A API nova é importada de react-dom e usada com o use:
O funcionamento é este:
- No servidor, o componente suspende e o
Suspensemais próximo coloca o fallback no HTML. - No browser, a chamada retorna
undefinede o render segue normal. Dá para usarlocalStorage,windowe companhia sem medo. - Se não tiver um Suspense acima, o render no servidor falha inteiro, em vez de cair num fallback vazio sem você perceber.
Esse último ponto eu acho ótimo. O React te obriga a decidir o que aparece enquanto o browser não assume.
Este blog já roda com o React 19.3, então dá para ver a API de verdade. O contador abaixo lê e grava no localStorage direto no render, sem useEffect e sem typeof window. Clica algumas vezes e recarrega a página:
Se você abrir o código-fonte da página, vai ver que no lugar do contador só tem o skeleton. Coisa linda! 🤩
Mas não resolve o que parece resolver
O use(browser()) não faz o componente existir no servidor. Ele continua tendo dois estados no tempo: o fallback e o conteúdo. E é o fallback que decide se a tela vai pular.
Se você passar null ali, voltou pro problema do mounted, só que com uma API nova:
Mesma API, mas agora com fallback={null}. O HTML do servidor não tem nada nesse espaço.
Com um skeleton do mesmo tamanho do componente, o espaço já vem reservado no HTML e nada se mexe quando o conteúdo chega. Com null, tudo que está abaixo desce.
Por que ainda gosto dela
Mesmo sem mágica, gostei da ideia. O que ela faz é te obrigar a escrever o fallback num lugar declarado, em vez de deixar a fronteira entre servidor e browser escondida num useEffect. Quem lê o componente vê o use(browser()) na primeira linha e já sabe o que está acontecendo.
Para biblioteca, isso importa ainda mais, porque vira uma linguagem comum. O Wasp já abriu uma issue para trocar o truque com useSyncExternalStore deles por essa API, justamente por causa do update síncrono e do re-render extra.
Fechando
Da próxima vez que você for escrever um typeof window ou um mounted, experimenta o use(browser()). Só não esquece de caprichar no fallback, porque é ele que o seu usuário vai ver primeiro.
A documentação da API é curta e vale a leitura. Se você já trocou algum hack por ela, me conta como ficou.