<h2>O Problema: Estado Externo e React</h2>
<p>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.</p>
<p>Antes do <code>useSyncExternalStore</code>, a abordagem comum era usar <code>useEffect</code> e <code>useState</code> 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 <code>useSyncExternalStore</code> resolve este problema de forma elegante e otimizada, permitindo que React controle completamente a sincronização com stores externas.</p>
<h2>Entendendo useSyncExternalStore</h2>
<h3>O que é e por que usar</h3>
<p><code>useSyncExternalStore</code> é 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.</p>
<p>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.</p>
<h3>Assinatura e Parâmetros</h3>
<pre><code class="language-javascript">const snapshot = useSyncExternalStore(
subscribe, // (callback) => unsubscribe
getSnapshot, // () => snapshot
getServerSnapshot // () => snapshot (opcional, para SSR)
);</code></pre>
<p>O parâmetro <code>subscribe</code> recebe um callback e deve retornar uma função que desinscreve o listener. O <code>getSnapshot</code> é chamado para obter o estado atual. O <code>getServerSnapshot</code> é usado apenas durante renderização no servidor, garantindo que o HTML inicial corresponda ao que será renderizado no cliente.</p>
<h2>Implementação Prática com Uma Store Customizada</h2>
<h3>Criando uma Store Simples</h3>
<p>Vamos criar uma store customizada do zero para entender completamente como <code>useSyncExternalStore</code> funciona:</p>
<pre><code class="language-javascript">// 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);
};
}
};</code></pre>
<p>Agora criamos um hook customizado que usa <code>useSyncExternalStore</code> para integrar esta store com React:</p>
<pre><code class="language-javascript">// 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())
);
}</code></pre>
<h3>Usando o Hook em Componentes</h3>
<pre><code class="language-javascript">// 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>
);
}</code></pre>
<p>Este padrão é excelente porque o componente só re-renderiza quando o seletor específico retorna um valor diferente. Se você seleciona apenas <code>count</code> e apenas <code>message</code> muda, não há re-render desnecessário.</p>
<h2>Integrando com Zustand e Outras Bibliotecas</h2>
<h3>Zustand Nativo</h3>
<p>A maioria das bibliotecas modernas já implementa <code>useSyncExternalStore</code> internamente. Zustand, por exemplo, oferece suporte nativo:</p>
<pre><code class="language-javascript">// 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 })
}));</code></pre>
<pre><code class="language-javascript">// 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>
);
}</code></pre>
<p>Zustand usa <code>useSyncExternalStore</code> internamente, então você obtém todos os benefícios de otimização e sincronização automática.</p>
<h3>Redux com useSyncExternalStore</h3>
<p>Para Redux, você pode criar um hook adaptador simples:</p>
<pre><code class="language-javascript">// 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())
);
}</code></pre>
<p>Embora o Redux já tenha integração com React hooks, usar <code>useSyncExternalStore</code> garante máxima compatibilidade com Concurrent Features.</p>
<h2>Otimizações e Melhores Práticas</h2>
<h3>Seletores e Performance</h3>
<p>A chave para performance é usar seletores bem definidos. Seletores ruins causam re-renders desnecessários:</p>
<pre><code class="language-javascript"></code></pre>
<p>Quando você seleciona apenas <code>count</code>, e apenas <code>message</code> muda na store, o componente não re-renderiza. Isso é possível porque <code>useSyncExternalStore</code> compara o snapshot anterior com o novo usando <code>Object.is()</code>.</p>
<h3>Evitando Race Conditions em SSR</h3>
<p>Para aplicações com Server-Side Rendering, sempre forneça <code>getServerSnapshot</code>:</p>
<pre><code class="language-javascript">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' });
}
);
}</code></pre>
<p>Sem <code>getServerSnapshot</code>, React avisa com warnings sobre mismatch entre servidor e cliente.</p>
<h3>Estruturando Stores para Múltiplas Subscrições</h3>
<p>Para stores grandes, é eficiente suportar seleções granulares:</p>
<pre><code class="language-javascript">// 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());
});
}
};</code></pre>
<h2>Conclusão</h2>
<p>O <code>useSyncExternalStore</code> 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.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://react.dev/reference/react/useSyncExternalStore" target="_blank" rel="noopener noreferrer">React useSyncExternalStore - Documentação Oficial</a></li>
<li><a href="https://github.com/pmndrs/zustand" target="_blank" rel="noopener noreferrer">Zustand GitHub - Biblioteca que implementa useSyncExternalStore</a></li>
<li><a href="https://redux-toolkit.js.org/" target="_blank" rel="noopener noreferrer">Redux Toolkit - Integração com React 18</a></li>
<li><a href="https://react.dev/blog/2022/03/29/react-v18" target="_blank" rel="noopener noreferrer">React 18 Concurrent Features</a></li>
<li><a href="https://tkdodo.eu/blog/sync-external-store" target="_blank" rel="noopener noreferrer">Ui libraries and useSyncExternalStore - Article by TkDodo</a></li>
</ul>
<p><!-- FIM --></p>