Sincronizando estado entre abas com BroadcastChannel e useSyncExternalStore
Por John Pires · 08 de março de 2026

Sincronizando estado entre abas com BroadcastChannel e useSyncExternalStore
Por que aprender isso em 2026
Contexto e importância
Em 2026 aplicações web continuam a crescer em complexidade: PWAs, ferramentas internas e apps com várias janelas/abas abertas pelo usuário. Manter consistência de estado entre abas evita confusão ao usuário, dados duplicados e condições de corrida simples — por exemplo, se o usuário altera uma configuração em uma aba, todas as outras devem refletir essa mudança.
Por que BroadcastChannel + useSyncExternalStore
BroadcastChannel é uma API de browser eficiente para comunicação entre contexts (abas, iframes e workers) do mesmo origin. Já o hook useSyncExternalStore do React oferece um padrão estável para integrar stores externas com o sistema de render do React (concurrent-safe, compatível com Suspense/Server Components quando aplicável). A combinação entrega sincronização entre abas com atualizações reativas e corretas para renderização.
Aprender essa combinação em 2026 é útil porque bibliotecas e frameworks continuam a adotar padrões reativos e o React incentiva o uso de stores externas padronizadas. A solução é leve (sem WebSockets) e funciona offline entre abas.
Dica: se sua aplicação precisa de sincronização entre clientes diferentes (usuários distintos), BroadcastChannel não serve — aí entram WebSockets, WebRTC ou backend. Aqui focamos em sincronização local entre abas do mesmo navegador e origin.
Configuração do ambiente
Ferramentas necessárias
Você precisa de Node.js (>=16 recomendado), um gerador de projetos React (Vite, Create React App ou Next.js) e um navegador moderno com suporte ao BroadcastChannel (Chrome, Edge, Firefox, Safari recente). Para começar rápido recomendo Vite por simplicidade.
Passo a passo para criar o projeto
1) Crie um projeto Vite React: yarn create vite my-app --template react ou npm create vite@latest my-app --template react.
2) Instale dependências: cd my-app && yarn ou npm install.
3) Inicie: yarn dev ou npm run dev e abra duas abas apontando para o dev server.
Explicação: você precisará de duas abas para testar sincronização. Vamos implementar um contador compartilhado entre abas, com persistência mínima (optional) e atualizações via BroadcastChannel.
Hello World progressivo
Objetivo do exemplo
Neste hello world progressivo vamos criar: (1) um BroadcastChannel simples que envia mensagens, (2) um store React que usa useSyncExternalStore para escutar esse canal, e (3) um componente que exibe e atualiza o contador, propagando mudanças para outras abas.
Primeiro: BroadcastChannel puro
Antes de integrar ao React, vamos entender a API do browser. O snippet abaixo cria um canal chamado 'count' e envia/escuta mensagens. Este código pode rodar no console do browser ou dentro de um módulo JS.
Contexto: cria o canal e loga mensagens recebidas. Útil para visualizar tráfego entre abas.
// Cria um BroadcastChannel para o nome 'count' e imprime mensagens recebidas
const channel = new BroadcastChannel('count');
channel.onmessage = (ev) => {
console.log('Mensagem recebida no canal count:', ev.data);
};
// Envia uma mensagem para outras abas
channel.postMessage({ type: 'PING', time: Date.now() });
// Fecha quando não precisar mais
// channel.close();
Depois: ao executar em duas abas do mesmo origin, a mensagem postMessage será recebida pela outra aba e registrada no console. Isso demonstra a comunicação básica e que o browser distribui mensagens entre contexts.
Integrando com useSyncExternalStore
Agora vamos criar um pequeno store que mantém um contador e notifica assinantes quando o contador muda. useSyncExternalStore precisa de três coisas: subscribe (como escutar mudanças), getSnapshot (valor atual) e um getServerSnapshot opcional para SSR. Para nosso caso client-side usamos subscribe + getSnapshot.
Contexto: o código abaixo mostra um pequeno módulo store.js que usa BroadcastChannel internamente. Ele exporta getSnapshot e subscribe para ser consumido pelo hook useSyncExternalStore no React.
import { useSyncExternalStore } from 'react';
// store.js
const channel = new BroadcastChannel('count');
let value = Number(localStorage.getItem('count') || 0);
channel.onmessage = (ev) => {
if (ev.data?.type === 'SET') {
value = ev.data.value;
// Não notifica aqui; a notificação virá via subscribe quando necessário
// No entanto, se alguém estiver usando subscribe, devemos chamar os listeners.
listeners.forEach((l) => l());
}
};
const listeners = new Set();
export function subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
export function getSnapshot() {
return value;
}
export function setCount(next) {
value = next;
localStorage.setItem('count', String(value));
// envia para outras abas
channel.postMessage({ type: 'SET', value });
// notifica listeners nesta aba
listeners.forEach((l) => l());
}
// Hook personalizado para usar no componente
export function useCount() {
return useSyncExternalStore(subscribe, getSnapshot);
}
Depois: importando useCount em um componente React, qualquer atualização via setCount será refletida imediatamente nesta aba e, graças ao BroadcastChannel, será enviada para outras abas que atualizarão seus snapshots e re-renderizarão.
Mini-projeto prático
Descrição do mini-projeto
Vamos construir um mini-contador compartilhado: um botão + e - que altera o contador. Quando o usuário clicar em uma aba, todas as outras abas atualizam automaticamente. Também salvaremos o valor no localStorage para persistência simples ao recarregar.
Componentes e fluxo
O projeto terá três arquivos principais: store.js (ou store.ts), Counter.jsx e main.jsx. store.js encapsula BroadcastChannel, assinantes e persistência. Counter.jsx usa o hook useCount e chama setCount para atualizar. main.jsx monta o app.
Abaixo um exemplo completo e funcional integrado. Coloque em src/store.js, src/Counter.jsx, src/main.jsx conforme instruções do seu bundler (Vite/CRA).
// src/store.js
import { useSyncExternalStore } from 'react';
const channel = new BroadcastChannel('count');
let value = Number(localStorage.getItem('count') || 0);
const listeners = new Set();
channel.onmessage = (ev) => {
if (ev.data?.type === 'SET') {
value = ev.data.value;
listeners.forEach((l) => l());
}
};
export function subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
export function getSnapshot() {
return value;
}
export function setCount(next) {
value = next;
localStorage.setItem('count', String(value));
channel.postMessage({ type: 'SET', value });
listeners.forEach((l) => l());
}
export function useCount() {
return useSyncExternalStore(subscribe, getSnapshot);
}
Contexto: este arquivo é o núcleo. Ele mantém o estado em memória (value), persiste em localStorage e usa BroadcastChannel para propagar mudanças. useSyncExternalStore conecta o store ao React oferecendo atualização segura.
// src/Counter.jsx
import React from 'react';
import { useCount, setCount } from './store';
export default function Counter() {
const count = useCount();
return (
Contador compartilhado Valor atual: {count} -+
);
}
Depois: importe e renderize <Counter /> em seu ponto de entrada (App/Main). Abra duas abas da aplicação; ao clicar + em uma, a outra atualiza imediatamente.
// src/main.jsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import Counter from './Counter';
createRoot(document.getElementById('root')).render();
Contexto: esse é o ponto de montagem da app. Simples e direto para testar a sincronização.
Erros comuns e armadilhas
Vamos enumerar problemas que iniciantes frequentemente encontram e como evitá-los.
Missão: múltiplos listeners e leaks
Erro: criar listeners (por subscribe) e não removê-los ao desmontar componentes. Resultado: memory leaks e callbacks duplicados. Use o retorno da função subscribe para remover ou garanta que seu hook encapsule remoção. No padrão useSyncExternalStore esse gerenciamento é esperado e o React cuida se subscribe retornar remover corretamente.
Missão: concorrência e estado inconsistente
Erro: assumir que localStorage é a fonte única de verdade sem sincronizá-lo com BroadcastChannel. Se duas abas atualizam quase ao mesmo tempo, você pode sobrescrever valor. Solução: sempre postar via BroadcastChannel e aplicar lógica de merge se necessário (por exemplo, usar timestamps ou CRDTs para dados complexos). Para um contador simples, aceitar 'último grava' é suficiente.
Dicas de performance
1) Evite enviar mensagens muito grandes pelo BroadcastChannel — serialize apenas o necessário. 2) Debounce ou throttle atualizações frequentes (por exemplo, digitação). 3) Para estruturas grandes, envie diffs em vez do objeto inteiro.
Implementação adicional: se precisar garantir ordenação, inclua um campo timestamp em cada mensagem e aplique somente updates mais recentes que o snapshot local.
Recursos para continuar estudando
Documentação e specs
Leia a MDN sobre BroadcastChannel e a RFC do useSyncExternalStore. MDN possui exemplos e compatibilidade por browser. O React docs explica o hook useSyncExternalStore e por que foi introduzido para suportar concurrent rendering e SSR-safe stores.
Exercícios e próximos passos
Exercícios práticos: (1) Adicionar autenticação e sincronizar preferências (tema claro/escuro); (2) Implementar locking simples entre abas (ex: garantir que apenas uma aba execute certa ação); (3) Combinar BroadcastChannel com Service Worker para sincronização mais persistente.
Implemente debounce no setCount para evitar flood de mensagens quando seu contador é alterado por um loop.
Teste em diferentes navegadores para garantir compatibilidade.
Considere fallback para localStorage events (window.addEventListener('storage')) em ambientes onde BroadcastChannel não exista.
Dica: Safari móvel pode ter restrições mais agressivas em relação a armazenamento e ciclo de vida das abas. Teste em alvo real.
Conclusao
Resumo prático
Você aprendeu a comunicar abas usando BroadcastChannel, integrar essa comunicação a um store consumível por React com useSyncExternalStore e construir um contador compartilhado. O padrão é leve, eficiente e adequado para muitas necessidades de sincronização local entre contexts do mesmo origin.
Checklist para aplicar em projetos reais
Encapsular BroadcastChannel em um módulo de store que exponha subscribe/getSnapshot/setState.
Usar useSyncExternalStore no React para conexão segura com o render.
Persistir em localStorage se precisar de reload; sincronizar persistência via mensagens.
Adicionar timestamps ou lógica de merge para conflitos, se os dados forem sensíveis.
Conclua testando a aplicação em múltiplas abas, browsers e cenários de atualização concorrente. Essa base permite evoluir para recursos mais sofisticados (CRDTs, sync via servidor) quando necessário, sem alterar a ergonomia do React.
Se quiser, entregue o mini-projeto em um repositório público e escreva testes para as funções do store (mock BroadcastChannel se necessário) — isso também solidifica o entendimento e previne regressões.
Bom trabalho. Foque em pequenos passos: isole a comunicação, garanta que subscribe/remove funcione, e depois integre com componentes React. Assim você reduz riscos e ganha confiança ao aplicar essa técnica em aplicações reais.
Assuntos: React, broadcastchannel, useSyncExternalStore, estado, Performance
Leia também
Sincronizar estado entre Server e Client Components com use e useSyncExternalStore
Padronize estado entre Server e Client Components usando use (servidor) e useSyncExternalStore (cliente). Evite fetch duplicado, hydrate sem mismatch e…
Suavizando transições em listas React com startTransition, useDeferredValue e Suspense
Como evitar travamentos em listas React: use startTransition para priorizar atualizações, useDeferredValue para adiar derivação de valores, e Suspense para…
useEffect vs useLayoutEffect no React: quando usar, exemplos e armadilhas
Compare useEffect e useLayoutEffect com exemplos práticos: quando usar cada um, medição de DOM sem layout shift, armadilhas comuns e padrões seguros para…
Criando useStableCallback (useEvent): callbacks estáveis no React
Aprenda a criar useStableCallback, um hook que fornece callbacks com identidade estável mas que chamam a versão mais recente do handler. Mostro o erro comum…
Corrigindo mismatches de hydration no React App Router (SSR): passo a passo
Guia prático e direto para identificar e corrigir mismatches de hydration no React App Router (SSR). Passo a passo: ambiente, exemplos, mini-projeto com…
Melhorar LCP com React Server Components e Suspense: streaming, placeholders e revalidação
Reduza LCP com React Server Components e Suspense: prioridades de dados, streaming incremental, placeholders que preservam layout e revalidação de cache…