Destino Dev

Blog

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

  1. Encapsular BroadcastChannel em um módulo de store que exponha subscribe/getSnapshot/setState.

  2. Usar useSyncExternalStore no React para conexão segura com o render.

  3. Persistir em localStorage se precisar de reload; sincronizar persistência via mensagens.

  4. 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