useSyncExternalStore inscreve um componente em dados que vivem fora do React e o renderiza de novo sempre que esses dados mudam. Você passa duas funções: subscribe, que diz ao React como ouvir as mudanças, e getSnapshot, que retorna o valor atual.
Os dois componentes Display não compartilham props nem context, mas atualizam juntos, porque os dois assinam a mesma store. O botão chama uma função comum, não um setter do React. Adicione um terceiro <Display name="Sidebar" /> e ele entra no jogo sem nenhuma outra mudança.
A sintaxe
const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);
subscribe(callback)começa a ouvir, chamacallbacksempre que os dados podem ter mudado e retorna uma função de cancelamento. O React a chama depois que o componente monta e chama a função retornada na desmontagem.getSnapshot()retorna o valor atual. O React a chama em toda renderização e depois de cada notificação e compara o resultado com o último usandoObject.is. Mesmo valor, nenhuma renderização.getServerSnapshot()(opcional) retorna o valor a usar no servidor e durante a hidratação.
Defina subscribe fora do componente ou mantenha-a estável com useCallback. Se você passa uma função subscribe nova a cada renderização, o React cancela e assina de novo toda vez.
APIs do navegador como stores
Qualquer coisa que tenha um valor atual e dispare um evento quando ele muda se encaixa nesse formato. O navigator.onLine com os eventos online e offline é o caso clássico.
Desligue a sua rede (ou mude para Offline nas ferramentas de desenvolvedor do navegador) e o texto muda sem recarregar. O terceiro argumento diz "considere online" ao renderizar no servidor, onde não existe navigator. Envolver a chamada do hook em useOnlineStatus o torna um hook personalizado que qualquer componente pode usar.
A largura da janela funciona do mesmo jeito:
Adicione algumas cópias e redimensione a janela: todas as cópias mostram o mesmo número no mesmo momento. Cada cópia tem o próprio listener, e cada uma lê a largura durante a renderização, então nenhuma fica um frame atrás.
O getSnapshot precisa retornar um valor em cache
O React chama getSnapshot com frequência e compara os resultados por referência. Uma função que monta um objeto ou array novo a cada chamada sempre parece uma mudança:
// Broken: a new object on every call
function getSnapshot() {
return { count: store.count, user: store.user };
}
// Also broken: filter returns a new array every time
function getSnapshot() {
return store.todos.filter((t) => !t.done);
}
O React renderiza, chama getSnapshot, recebe um valor "diferente", renderiza de novo, e assim por diante, até parar com "Maximum update depth exceeded". Em desenvolvimento o React também registra antes "The result of getSnapshot should be cached to avoid an infinite loop". Um build de produção, como a prévia aqui, pula esse aviso e só reporta o erro final como um código curto (Minified React error #185).
A correção é manter os dados imutáveis na store: substitua o objeto quando ele mudar e retorne a referência guardada como está.
O getSnapshot retorna o mesmo objeto state até add substituí-lo, então o componente renderiza uma vez por mudança. A filtragem acontece no componente, depois de o snapshot ser lido, o que é seguro. Para filtrar dentro da store, calcule o array filtrado quando os dados mudarem e guarde-o, para que o getSnapshot possa retornar a cópia guardada.
getServerSnapshot e hidratação
No servidor não há janela, nem navigator, nem assinatura. O terceiro argumento diz ao React o que renderizar ali:
const width = useSyncExternalStore(
subscribe,
() => window.innerWidth, // in the browser
() => 1024 // on the server, and during hydration
);
O React também usa o getServerSnapshot na primeira renderização no navegador, quando hidrata o HTML do servidor, para que os dois coincidam. Logo depois da hidratação ele lê o getSnapshot e, se o valor real for diferente, renderiza de novo com ele. Sem o terceiro argumento, a renderização no servidor lança "Missing getServerSnapshot, which is required for server-rendered content. Will revert to client rendering." Se houver um <Suspense> acima do componente, o servidor envia o fallback desse boundary e o navegador renderiza o conteúdo; sem boundary, a renderização no servidor falha.
useSyncExternalStore vs useEffect e useState
Você pode assinar com um efeito:
function useOnlineStatus() {
const [online, setOnline] = useState(true);
useEffect(() => {
const update = () => setOnline(navigator.onLine);
update();
window.addEventListener('online', update);
window.addEventListener('offline', update);
return () => {
window.removeEventListener('online', update);
window.removeEventListener('offline', update);
};
}, []);
return online;
}
Funciona, com duas fraquezas. A primeira renderização sempre mostra o palpite inicial, e o valor real chega uma renderização depois, quando o efeito executa. E com renderização concorrente (durante uma transição, por exemplo), o React pode pausar uma renderização no meio; se a store mudar durante a pausa, componentes renderizados antes e depois podem mostrar valores diferentes. Essa inconsistência se chama tearing. O useSyncExternalStore lê o valor durante a renderização e faz o React refazer a renderização de forma síncrona se a store mudou, então todo componente vê o mesmo valor.
Use-o quando os dados vivem fora do React: o seu próprio módulo de store, uma API do navegador, uma biblioteca de terceiros. A maioria das bibliotecas de estado (Redux, Zustand e outras) o chama por você dentro dos próprios hooks. Para dados que pertencem aos seus componentes, useState, useReducer e context continuam sendo as ferramentas certas.
Perguntas frequentes
Para que serve o useSyncExternalStore?
Para ler dados que o React não possui e que podem mudar sozinhos: uma store escrita fora do React, uma biblioteca de estado de terceiros ou um valor do navegador como navigator.onLine ou a largura da janela. O componente renderiza de novo sempre que a store avisa o React de que mudou.
O que fazem subscribe e getSnapshot?
subscribe(callback) começa a ouvir a store, chama callback a cada mudança e retorna uma função que para de ouvir. getSnapshot() retorna o valor atual. O React chama getSnapshot durante a renderização e depois de cada notificação, e só renderiza de novo se o valor mudou segundo Object.is.
Por que o getSnapshot precisa retornar um valor em cache?
O React compara o resultado de cada chamada de getSnapshot com o anterior. Se ele retorna um objeto ou array novo toda vez, o React sempre vê uma mudança, renderiza de novo, chama getSnapshot de novo e entra em loop até lançar "Maximum update depth exceeded". Retorne a mesma referência até os dados realmente mudarem.
O que é o getServerSnapshot?
O terceiro argumento opcional. Ele retorna o valor a usar durante a renderização no servidor e durante a hidratação no navegador, para que os dois produzam o mesmo HTML. Sem ele, o componente lança "Missing getServerSnapshot" no servidor, e o conteúdo abaixo do <Suspense> mais próximo é renderizado no navegador.
Devo usar useSyncExternalStore ou useEffect com useState?
Para assinar dados externos, prefira o useSyncExternalStore. Ele lê o valor durante a renderização, então a primeira renderização já sai correta e todo componente vê o mesmo valor, mesmo durante a renderização concorrente. O useEffect com useState renderiza uma vez com um valor desatualizado e pode mostrar, por um instante, valores diferentes em componentes diferentes.