React & Frontend

Guia Completo de useSyncExternalStore: Integrando Stores Externas com React

9 min de leitura

Guia Completo de useSyncExternalStore: Integrando Stores Externas com React

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

<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) =&gt; unsubscribe

getSnapshot, // () =&gt; snapshot

getServerSnapshot // () =&gt; 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: &#039;Olá&#039; };

let listeners = [];

export const store = {

getState: () =&gt; state,

setState: (newState) =&gt; {

state = { ...state, ...newState };

listeners.forEach(listener =&gt; listener());

},

subscribe: (listener) =&gt; {

listeners.push(listener);

return () =&gt; {

listeners = listeners.filter(l =&gt; 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 &#039;react&#039;;

import { store } from &#039;./store&#039;;

export function useStore(selector) {

return useSyncExternalStore(

(callback) =&gt; store.subscribe(callback),

() =&gt; selector(store.getState()),

() =&gt; selector(store.getState())

);

}</code></pre>

<h3>Usando o Hook em Componentes</h3>

<pre><code class="language-javascript">// Counter.js

import { useStore } from &#039;./useStore&#039;;

import { store } from &#039;./store&#039;;

export function Counter() {

const count = useStore(state =&gt; state.count);

const message = useStore(state =&gt; state.message);

return (

&lt;div&gt;

&lt;h1&gt;{count}&lt;/h1&gt;

&lt;p&gt;{message}&lt;/p&gt;

&lt;button onClick={() =&gt; store.setState({ count: count + 1 })}&gt;

Incrementar

&lt;/button&gt;

&lt;button onClick={() =&gt; store.setState({ message: &#039;Atualizado!&#039; })}&gt;

Atualizar Mensagem

&lt;/button&gt;

&lt;/div&gt;

);

}</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 &#039;zustand&#039;;

export const useCounterStore = create((set) =&gt; ({

count: 0,

message: &#039;Olá&#039;,

increment: () =&gt; set((state) =&gt; ({ count: state.count + 1 })),

setMessage: (msg) =&gt; set({ message: msg })

}));</code></pre>

<pre><code class="language-javascript">// ComponenteComZustand.js

import { useCounterStore } from &#039;./zustand-store&#039;;

export function Counter() {

const count = useCounterStore((state) =&gt; state.count);

const message = useCounterStore((state) =&gt; state.message);

const increment = useCounterStore((state) =&gt; state.increment);

const setMessage = useCounterStore((state) =&gt; state.setMessage);

return (

&lt;div&gt;

&lt;h1&gt;{count}&lt;/h1&gt;

&lt;p&gt;{message}&lt;/p&gt;

&lt;button onClick={increment}&gt;Incrementar&lt;/button&gt;

&lt;button onClick={() =&gt; setMessage(&#039;Novo!&#039;)}&gt;Atualizar&lt;/button&gt;

&lt;/div&gt;

);

}</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 &#039;react&#039;;

import { useSelector } from &#039;react-redux&#039;;

export function useSyncRedux(selector) {

const store = useSelector(state =&gt; state);

return useSyncExternalStore(

(callback) =&gt; {

const unsubscribe = store.subscribe(callback);

return unsubscribe;

},

() =&gt; selector(store.getState()),

() =&gt; 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) =&gt; store.subscribe(callback),

() =&gt; selector(store.getState()),

() =&gt; {

// Durante SSR, retorne um estado inicial previsível

return selector({ count: 0, message: &#039;Inicial&#039; });

}

);

}</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: &#039;João&#039;, age: 30 }, posts: [] };

let listeners = new Map();

export const store = {

getState: () =&gt; state,

subscribe: (listener, selector) =&gt; {

if (!listeners.has(selector)) {

listeners.set(selector, []);

}

listeners.get(selector).push(listener);

return () =&gt; {

const list = listeners.get(selector);

const index = list.indexOf(listener);

if (index &gt; -1) list.splice(index, 1);

};

},

setState: (newState) =&gt; {

state = { ...state, ...newState };

// Notifica apenas listeners relevantes

listeners.forEach((list) =&gt; {

list.forEach(listener =&gt; 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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

Boas Práticas de Testing Library em Profundidade: Queries, Fire Events e Async para Times Ágeis
Boas Práticas de Testing Library em Profundidade: Queries, Fire Events e Async para Times Ágeis

Testing Library em Profundidade: Queries, Fire Events e Async Testing Library...

Controlled vs Uncontrolled Components: Quando Usar Cada Abordagem na Prática
Controlled vs Uncontrolled Components: Quando Usar Cada Abordagem na Prática

O Que São Componentes Controlados e Não Controlados? Antes de mais nada, prec...

React Fiber: Arquitetura Interna, Reconciliation e Rendering Phases: Do Básico ao Avançado
React Fiber: Arquitetura Interna, Reconciliation e Rendering Phases: Do Básico ao Avançado

React Fiber: Arquitetura Interna, Reconciliation e Rendering Phases React Fib...