Guia Completo de useSyncExternalStore: Integrando Stores Externas com React Já leu

O Problema: Estado Externo e React Quando trabalhamos com React, frequentemente precisamos integrar stores externas — bibliotecas como Redux, Zustand, MobX ou até mesmo APIs de estado customizadas. O desafio é garantir que os componentes React se atualizem corretamente quando o estado externo muda, mantendo a performance e evitando re-renders desnecessários. Antes do , a abordagem comum era usar e para sincronizar manualmente o estado externo com o estado local do componente. Isso funcionava, mas criava overhead: você precisava gerenciar subscrições, desinscrições, e lidar com race conditions. O hook resolve este problema de forma elegante e otimizada, permitindo que React controle completamente a sincronização com stores externas. Entendendo useSyncExternalStore O que é e por que usar é um hook do React que sincroniza dados de uma store externa com o estado do componente. Ele foi introduzido no React 18 especificamente para resolver problemas de tearing (inconsistência visual) e garantir que o código funcione corretamente com Concurrent Features do React. O

O Problema: Estado Externo e React

Quando trabalhamos com React, frequentemente precisamos integrar stores externas — bibliotecas como Redux, Zustand, MobX ou até mesmo APIs de estado customizadas. O desafio é garantir que os componentes React se atualizem corretamente quando o estado externo muda, mantendo a performance e evitando re-renders desnecessários.

Antes do useSyncExternalStore, a abordagem comum era usar useEffect e useState para sincronizar manualmente o estado externo com o estado local do componente. Isso funcionava, mas criava overhead: você precisava gerenciar subscrições, desinscrições, e lidar com race conditions. O hook useSyncExternalStore resolve este problema de forma elegante e otimizada, permitindo que React controle completamente a sincronização com stores externas.

Entendendo useSyncExternalStore

O que é e por que usar

useSyncExternalStore é um hook do React que sincroniza dados de uma store externa com o estado do componente. Ele foi introduzido no React 18 especificamente para resolver problemas de tearing (inconsistência visual) e garantir que o código funcione corretamente com Concurrent Features do React.

O hook requer três argumentos principais: uma função para se inscrever nas mudanças, uma função para obter o snapshot do estado atual, e opcionalmente uma função para obter o snapshot do servidor (importante para SSR). Quando a store externa muda, React automaticamente re-renderiza o componente com o novo snapshot, garantindo consistência.

Assinatura e Parâmetros

const snapshot = useSyncExternalStore(
  subscribe,        // (callback) => unsubscribe
  getSnapshot,      // () => snapshot
  getServerSnapshot // () => snapshot (opcional, para SSR)
);

O parâmetro subscribe recebe um callback e deve retornar uma função que desinscreve o listener. O getSnapshot é chamado para obter o estado atual. O getServerSnapshot é usado apenas durante renderização no servidor, garantindo que o HTML inicial corresponda ao que será renderizado no cliente.

Implementação Prática com Uma Store Customizada

Criando uma Store Simples

Vamos criar uma store customizada do zero para entender completamente como useSyncExternalStore funciona:

// store.js
let state = { count: 0, message: 'Olá' };
let listeners = [];

export const store = {
  getState: () => state,

  setState: (newState) => {
    state = { ...state, ...newState };
    listeners.forEach(listener => listener());
  },

  subscribe: (listener) => {
    listeners.push(listener);
    return () => {
      listeners = listeners.filter(l => l !== listener);
    };
  }
};

Agora criamos um hook customizado que usa useSyncExternalStore para integrar esta store com React:

// useStore.js
import { useSyncExternalStore } from 'react';
import { store } from './store';

export function useStore(selector) {
  return useSyncExternalStore(
    (callback) => store.subscribe(callback),
    () => selector(store.getState()),
    () => selector(store.getState())
  );
}

Usando o Hook em Componentes

// Counter.js
import { useStore } from './useStore';
import { store } from './store';

export function Counter() {
  const count = useStore(state => state.count);
  const message = useStore(state => state.message);

  return (
    <div>
      <h1>{count}</h1>
      <p>{message}</p>
      <button onClick={() => store.setState({ count: count + 1 })}>
        Incrementar
      </button>
      <button onClick={() => store.setState({ message: 'Atualizado!' })}>
        Atualizar Mensagem
      </button>
    </div>
  );
}

Este padrão é excelente porque o componente só re-renderiza quando o seletor específico retorna um valor diferente. Se você seleciona apenas count e apenas message muda, não há re-render desnecessário.

Integrando com Zustand e Outras Bibliotecas

Zustand Nativo

A maioria das bibliotecas modernas já implementa useSyncExternalStore internamente. Zustand, por exemplo, oferece suporte nativo:

// zustand-store.js
import { create } from 'zustand';

export const useCounterStore = create((set) => ({
  count: 0,
  message: 'Olá',
  increment: () => set((state) => ({ count: state.count + 1 })),
  setMessage: (msg) => set({ message: msg })
}));
// ComponenteComZustand.js
import { useCounterStore } from './zustand-store';

export function Counter() {
  const count = useCounterStore((state) => state.count);
  const message = useCounterStore((state) => state.message);
  const increment = useCounterStore((state) => state.increment);
  const setMessage = useCounterStore((state) => state.setMessage);

  return (
    <div>
      <h1>{count}</h1>
      <p>{message}</p>
      <button onClick={increment}>Incrementar</button>
      <button onClick={() => setMessage('Novo!')}>Atualizar</button>
    </div>
  );
}

Zustand usa useSyncExternalStore internamente, então você obtém todos os benefícios de otimização e sincronização automática.

Redux com useSyncExternalStore

Para Redux, você pode criar um hook adaptador simples:

// redux-adapter.js
import { useSyncExternalStore } from 'react';
import { useSelector } from 'react-redux';

export function useSyncRedux(selector) {
  const store = useSelector(state => state);

  return useSyncExternalStore(
    (callback) => {
      const unsubscribe = store.subscribe(callback);
      return unsubscribe;
    },
    () => selector(store.getState()),
    () => selector(store.getState())
  );
}

Embora o Redux já tenha integração com React hooks, usar useSyncExternalStore garante máxima compatibilidade com Concurrent Features.

Otimizações e Melhores Práticas

Seletores e Performance

A chave para performance é usar seletores bem definidos. Seletores ruins causam re-renders desnecessários:

// ❌ RUIM - retorna um novo objeto a cada chamada
const useStore = (selector) => useSyncExternalStore(
  (cb) => store.subscribe(cb),
  () => ({ count: store.getState().count, message: store.getState().message })
);

// ✅ BOM - retorna apenas o que é necessário
const useStore = (selector) => useSyncExternalStore(
  (cb) => store.subscribe(cb),
  () => selector(store.getState())
);

Quando você seleciona apenas count, e apenas message muda na store, o componente não re-renderiza. Isso é possível porque useSyncExternalStore compara o snapshot anterior com o novo usando Object.is().

Evitando Race Conditions em SSR

Para aplicações com Server-Side Rendering, sempre forneça getServerSnapshot:

export function useStore(selector) {
  return useSyncExternalStore(
    (callback) => store.subscribe(callback),
    () => selector(store.getState()),
    () => {
      // Durante SSR, retorne um estado inicial previsível
      return selector({ count: 0, message: 'Inicial' });
    }
  );
}

Sem getServerSnapshot, React avisa com warnings sobre mismatch entre servidor e cliente.

Estruturando Stores para Múltiplas Subscrições

Para stores grandes, é eficiente suportar seleções granulares:

// advanced-store.js
let state = { user: { name: 'João', age: 30 }, posts: [] };
let listeners = new Map();

export const store = {
  getState: () => state,

  subscribe: (listener, selector) => {
    if (!listeners.has(selector)) {
      listeners.set(selector, []);
    }
    listeners.get(selector).push(listener);

    return () => {
      const list = listeners.get(selector);
      const index = list.indexOf(listener);
      if (index > -1) list.splice(index, 1);
    };
  },

  setState: (newState) => {
    state = { ...state, ...newState };
    // Notifica apenas listeners relevantes
    listeners.forEach((list) => {
      list.forEach(listener => listener());
    });
  }
};

Conclusão

O useSyncExternalStore resolve um problema real: manter React sincronizado com estado externo de forma segura e performática. Ele garante que você não enfrente tearing (inconsistência visual) e que seu código funcione corretamente com Concurrent Features. Em segundo lugar, entender este hook aprofunda sua compreensão de como React gerencia subscriptions e sincronização — conhecimento valioso mesmo ao usar bibliotecas que já o implementam internamente, como Zustand e Redux Toolkit. Por fim, a implementação correta com seletores granulares é fundamental: o hook compara snapshots automaticamente, então use-o para evitar re-renders desnecessários selecionando apenas os dados que seu componente realmente precisa.

Referências


Artigos relacionados