Destino Dev

Blog

Context selectors: reduzir re-renders e otimizar estado global no React

Por John Pires · 12 de março de 2026

Context selectors: reduzir re-renders e otimizar estado global no React

Context selectors são uma técnica e um conjunto de padrões que permitem usar o Context do React sem pagar o custo de re-renders desnecessários quando partes não relacionadas do estado mudam. Este artigo aborda quando aplicar selectors, compara alternativas, mostra implementações progressivas — do padrão ingênuo ao uso de useSyncExternalStore — e integra o padrão com bibliotecas reais e arquiteturas contemporâneas. O foco é prático: reduz re-renders e mantém código previsível em apps intermediários a grandes.

1) Quando e por que usar

Context é ótimo para evitar prop drilling e agrupar estado global (tema, usuário, configurações). Porém, o comportamento padrão do Context faz com que todos os consumidores renderizem quando o valor do Provider muda por referencial — mesmo quando o consumidor só usa uma fatia do valor. Context selectors resolvem isso permitindo que componentes leiam apenas uma parte do estado e subscrevam mudanças dessa fatia específica, evitando re-renders quando outras partes do estado mudam.

Use selectors quando:

  • Você tem um Provider com um objeto grande ou vários campos que mudam independentemente.

  • Componentes filhos consomem apenas sub-ramificações do estado.

  • Percebe re-renders em árvore mesmo quando dados não usados mudam.

Impacto prático

Em aplicações reais, reduzir re-renders pode melhorar frames por segundo em listas grandes, reduzir trabalho de reconciler e evitar execuções desnecessárias de efeitos. Isso melhora latência aparente e responsividade.

Quando não usar

Se sua aplicação tem um Provider pequeno (por exemplo, apenas um booleano) ou mudanças raras e previsíveis, selectors introduzem complexidade desnecessária. Comece simples e só adote selectors quando mensurar impacto.

2) Comparação com alternativas

Existem várias estratégias para reduzir re-renders com estado global. Vou comparar as mais comuns: dividir contexts, memoização, use-context-selector (lib), useSyncExternalStore, e stores como Zustand ou Redux.

Dividir contextos

Separar cada fatia de estado em um Context diferente é a técnica mais direta. Quando você tem 3 pedaços de estado, crie 3 Providers e consuma apenas o necessário. Isso evita re-renders entre fatias, mas aumenta boilerplate e pode tornar difícil compor dados que vêm de múltiplos contexts.

Memoização e callbacks

Memoizar objetos com useMemo e callbacks com useCallback ajuda, mas não resolve quando o Provider precisa atualizar o objeto por motivos legítimos. Memo evita mudanças de referência entre renders, mas se algum campo interno mudar você ainda causa re-render global do Context.

Libraries: use-context-selector vs Zustand vs Redux

use-context-selector (pacote) amplia Context com seletores: você passa uma função que seleciona a parte do valor, e o hook só re-renderiza quando essa parte muda. É baixo custo conceitual e integra bem com APIs do React.

Zustand implementa uma store global baseada em hooks com suporte nativo a seletores e subscrições. Muito eficiente e de baixo boilerplate para stores reativos.

Redux com hooks já tem selectors memoizados (reselect) e a store é otimizada para subscrições. É uma solução madura para apps grandes, mas traz mais arquitetura.

3) Implementação progressiva

Vou mostrar três implementações concretas: 1) Padrão ingênuo com createContext; 2) Uso de use-context-selector (biblioteca) para selectors; 3) Implementação com useSyncExternalStore para criar uma store selecionável sem bibliotecas.

3.1 Padrão ingênuo (problema demonstrado)

Context simples: Provider passa um objeto com múltiplas propriedades e consumidores usam useContext. Quando qualquer campo muda, todos os consumidores re-renderizam mesmo que não usem o campo alterado.

O código abaixo cria um contador global e uma configuração (theme). Ambos vivem no mesmo Provider e ilustram o problema.

// O que faz: cria um Provider com estado combinado (counter, theme). Consumidor re-renderiza quando qualquer campo muda.
import React, { createContext, useContext, useState } from 'react';

const AppContext = createContext(null);

export function AppProvider({ children }) {
  const [counter, setCounter] = useState(0);
  const [theme, setTheme] = useState('light');

  const value = { counter, setCounter, theme, setTheme };

  return {children};
}

export function useAppContext() {
  return useContext(AppContext);
}

// Consumidor:
export function CounterDisplay() {
  const { counter } = useAppContext();
  return Counter: {counter};
}

export function ThemeToggle() {
  const { theme, setTheme } = useAppContext();
  return  setTheme(t => (t === 'light' ? 'dark' : 'light'))}>{theme};
}

O que acontece: se ThemeToggle chama setTheme, React atualiza o value (novo objeto) e todos os consumidores que usam useContext(AppContext) re-renderizam — incluindo CounterDisplay, mesmo que counter não tenha mudado.

3.2 Uso de use-context-selector (solução direta)

Context selector elimina re-renders ao permitir subscrever apenas a fatia necessária. A biblioteca use-context-selector fornece createContext e useContextSelector. Abaixo um exemplo equivalente ao anterior, porém o CounterDisplay só re-renderiza quando counter mudar.

// O que faz: usa 'use-context-selector' para selecionar apenas counter ou theme.
import React from 'react';
import { createContext, useContextSelector } from 'use-context-selector';

const AppContext = createContext(null);

export function AppProvider({ children }) {
  const [counter, setCounter] = React.useState(0);
  const [theme, setTheme] = React.useState('light');
  const value = React.useMemo(() => ({ counter, setCounter, theme, setTheme }), [counter, theme]);
  return {children};
}

export function CounterDisplay() {
  // seleciona apenas counter
  const counter = useContextSelector(AppContext, v => v.counter);
  return Counter: {counter};
}

export function ThemeToggle() {
  const theme = useContextSelector(AppContext, v => v.theme);
  const setTheme = useContextSelector(AppContext, v => v.setTheme);
  return  setTheme(t => (t === 'light' ? 'dark' : 'light'))}>{theme};
}


O que acontece: quando setTheme muda theme, somente consumidores que selecionam theme serão atualizados. CounterDisplay não re-renderiza.

Tip: se você usa TypeScript, tipar bem o contexto evita bugs ao selecionar campos inexistentes — prefira criar interfaces explícitas para o value.

3.3 Implementação com useSyncExternalStore (sem libs)

useSyncExternalStore é a API do React para subscrições externas e funciona bem para criar stores com selectors sem depender de bibliotecas. Vou mostrar uma store simples que expõe getState, setState e subscribe, e um hook useStoreSelector que usa useSyncExternalStore para subscrever apenas a fatia selecionada.

// O que faz: implementa uma store mínima e um hook selector que usa useSyncExternalStore.
import React from 'react';
import { useSyncExternalStore } from 'react';

function createStore(initial) {
  let state = initial;
  const listeners = new Set();

  return {
    getState: () => state,
    setState: (updater) => {
      const next = typeof updater === 'function' ? updater(state) : updater;
      if (next === state) return;
      state = next;
      listeners.forEach(l => l());
    },
    subscribe: (listener) => {
      listeners.add(listener);
      return () => listeners.delete(listener);
    }
  };
}

const appStore = createStore({ counter: 0, theme: 'light' });

export function useStoreSelector(selector) {
  return useSyncExternalStore(
    appStore.subscribe,
    () => selector(appStore.getState()),
    () => selector(appStore.getState())
  );
}

// Uso:
export function Counter() {
  const counter = useStoreSelector(s => s.counter);
  return {counter};
}

export function increment() {
  appStore.setState(s => ({ ...s, counter: s.counter + 1 }));
}


O que acontece: cada hook useStoreSelector subscreve a store e usa o selector para obter apenas a fatia desejada. Quando setState é chamado, listeners são notificados, mas o reconciler só re-renderiza os componentes cujos valores de selector mudaram comparando snapshots (useSyncExternalStore fornece estabilidade para render server/client).

Erros comuns e armadilhas

Implementar selectors tem pegadinhas que geram bugs sutis. Conheça as principais:

  • Selector não puro: selectors devem ser determinísticos e sem efeitos. Evite computações que dependam de variáveis externas mutáveis.

  • Comparação por referência: se seu selector retorna objetos novos a cada render, o componente verá mudança mesmo quando dados equivalentes. Memoize ou retorne primitivos/valores estáveis.

  • Atualizações por referência no Provider: no padrão com createContext, recriar o value em cada render causará re-renders — memoize com useMemo.

  • Assincronismo e Suspense: ao usar dados remotos, garanta que o selector e a store lidem com estados de loading e error de forma consistente para evitar estados inconsistentes em múltiplos consumidores.

Erros acima explicam por que libraries maduras adotaram APIs específicas para selectors: elas cuidam de assinaturas, de estabilidade e de comparação.

4) Integração com ecossistema real

Context selectors encaixam em várias arquiteturas e bibliotecas. Vou mostrar integrações com: Suspense/Server Components, Redux/Zustand e testes (React Testing Library).

4.1 Server Components e Suspense

Server Components (RSC) podem fornecer dados pré-hidratados. Quando você precisa de uma store híbrida (server + client), mantenha a store no cliente para interatividade e hydrate com dados do servidor. Use selectors para que a hidratação inicial não cause re-renders massivos — injete valores iniciais estáveis e só atualize campos que diferem explicitamente.

Exemplo de integração: RSC renderiza props iniciais, Provider no cliente pega esses valores e inicia a store. useSyncExternalStore é compatível com Hydration e concurrent rendering.

4.2 Integração com Zustand

Zustand já implementa selectors e é uma alternativa leve. Exemplo rápido de como usar selectors com Zustand, comparado ao padrão Context.

// O que faz: cria uma store Zustand e consome com selector para evitar re-renders.
import create from 'zustand';

const useStore = create(set => ({
  counter: 0,
  theme: 'light',
  increment: () => set(s => ({ counter: s.counter + 1 })),
  toggleTheme: () => set(s => ({ theme: s.theme === 'light' ? 'dark' : 'light' }))
}));

export function CounterZustand() {
  const counter = useStore(s => s.counter);
  return {counter};
}

export function ThemeZustand() {
  const theme = useStore(s => s.theme);
  const toggle = useStore(s => s.toggleTheme);
  return {theme};
}


O que acontece: Zustand notifica apenas assinantes cujos seletores retornam valores diferentes. Performance semelhante à solução com useSyncExternalStore, com menos boilerplate.

4.3 Testes e diagnósticos

Ao testar componentes que usam selectors, prefira renderizar o Provider/Store real e simular mudanças por setters ou pela store. Use ferramentas como React DevTools profiler e why-did-you-render (quando aplicável) para identificar re-renders inesperados.

Dica de mentoria: instrumente os componentes críticos com console.count em um useEffect para medir renders durante desenvolvimento antes de aplicar otimizações.

5) Checklist de qualidade para produção

Antes de levantar um padrão de selectors em produção, valide uma checklist técnica que evita regressões e mantêm a consistência:

  1. Métrica de baseline: meça re-renders e tempo de frame antes e depois da mudança.

  2. Selectors puros: garanta que os selectors sejam funções puras e rápidas.

  3. Estabilidade de objeto: evite retornar objetos recomputados sem memoização.

  4. Hydration segura: verifique compatibilidade com SSR/RSC e useSyncExternalStore quando necessário.

  5. Documentação: documente o contrato do contexto/store e exemplos de uso para novos devs.

  6. Observability: adicione métricas simples (counts de renders) nos componentes críticos para detectar regressão em produção.

  7. Fallbacks testados: cover cases de erro/network e states nulos.

  8. Limpeza de memória: garanta unsubscribe/cleanup para stores customizadas.

Esses itens reduzem risco e garantem que a otimização traga benefício real sem custos ocultos.

Checklist rápido integrado

  • Medir antes/depois com React Profiler

  • Garantir selectors são O(1) ou O(n pequeno)

  • Adicionar testes unitários que alterem campos não relacionados e assert: consumidor não re-renderizou

  • Evitar múltiplos re-renders por atualização síncrona — batch updates e useTransition para expensive updates

Conclusão prática

Context selectors são uma ferramenta prática para reduzir re-renders quando se usa estado global no React. Comece medindo; se notar re-renders desnecessários, prefira soluções graduais: dividir contexts, aplicar memoization, depois migrar para use-context-selector ou useSyncExternalStore. Para novos projetos, considerar Zustand ou outra store com selector integrado reduz trabalho manual.

Implementações simples podem parecer suficientes, mas falhas sutis (comparação por referência, selectors impuros, hydration inconsistente) geram bugs difíceis de rastrear. Adote a checklist e automatize métricas para garantir ganhos reais de performance.

Por fim, a escolha entre usar uma biblioteca (use-context-selector, zustand) ou implementar sua própria store depende de trade-offs: tempo, previsibilidade e equipe. Para times pequenos e projetos críticos em performance, prefira bibliotecas maduras; para casos pontuais, useSyncExternalStore entrega controle e compatibilidade com o concurrent renderer.

Use o padrão com disciplina: selectors bem definidos, selectors puros e comparação estável — essa tríade evita a maior parte das armadilhas e garante que seu app escale sem renderizar demais.

Se quiser, posso gerar um checklist automatizável (script) para medir renders com React Profiler ou transformar os exemplos em um pacote utilitário leve para seu monorepo.

Assuntos: React, Performance, context, hooks, state

Leia também