Menu

useSyncExternalStore no React: assine dados externos

useSyncExternalStore inscreve um componente em dados que vivem fora do React, como um pequeno módulo de store ou uma API do navegador como navigator.onLine, e renderiza de novo sempre que esses dados mudam. Aprenda subscribe e getSnapshot, por que o snapshot precisa ficar em cache e o getServerSnapshot.

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

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, chama callback sempre 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 usando Object.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.

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

Aprenda a programar com o Coddy

COMEÇAR