useContext Avançado: Performance, Splitting e Evitando Re-renders na Prática Já leu

O Problema Real do useContext em Aplicações Escaláveis Quando iniciamos com React e descobrimos o , parece uma solução mágica para evitar prop drilling. No entanto, conforme sua aplicação cresce, você descobrirá um problema fundamental: o causa re-renders em todos os componentes que o consomem, independentemente de qual parte do estado eles realmente utilizam. Isso não é um bug; é o comportamento padrão do React, e ignorar essa realidade custará performance. Imagine uma aplicação com um Context global contendo dados de usuário, tema, notificações e configurações. Se você tiver 50 componentes consumindo esse Context e apenas o tema mudar, todos esses 50 componentes sofrerão re-render, mesmo que 45 deles não usem a informação de tema. Essa cascata de re-renders desnecessários é exatamente o que exploraremos para resolver. Context Splitting: Dividindo para Vencer O Conceito Fundamental Context Splitting é a prática de dividir um grande Context em múltiplos Contexts menores e especializados. Em vez de ter um monolítico, você cria ,

O Problema Real do useContext em Aplicações Escaláveis

Quando iniciamos com React e descobrimos o useContext, parece uma solução mágica para evitar prop drilling. No entanto, conforme sua aplicação cresce, você descobrirá um problema fundamental: o useContext causa re-renders em todos os componentes que o consomem, independentemente de qual parte do estado eles realmente utilizam. Isso não é um bug; é o comportamento padrão do React, e ignorar essa realidade custará performance.

Imagine uma aplicação com um Context global contendo dados de usuário, tema, notificações e configurações. Se você tiver 50 componentes consumindo esse Context e apenas o tema mudar, todos esses 50 componentes sofrerão re-render, mesmo que 45 deles não usem a informação de tema. Essa cascata de re-renders desnecessários é exatamente o que exploraremos para resolver.

Context Splitting: Dividindo para Vencer

O Conceito Fundamental

Context Splitting é a prática de dividir um grande Context em múltiplos Contexts menores e especializados. Em vez de ter um AppContext monolítico, você cria ThemeContext, UserContext, NotificationContext etc. Cada componente então consome apenas os Contexts que realmente precisa, reduzindo drasticamente os re-renders.

A lógica é simples: se um componente só precisa do tema, deve estar conectado apenas ao ThemeContext. Quando o tema muda, somente os consumidores do ThemeContext sofrem re-render.

Exemplo Prático: Antes do Splitting

// ❌ Contexto monolítico - problema de performance
import React, { createContext, useState } from 'react';

export const AppContext = createContext();

export function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);
  const [settings, setSettings] = useState({});

  return (
    <AppContext.Provider value={{ user, setUser, theme, setTheme, notifications, setNotifications, settings, setSettings }}>
      {children}
    </AppContext.Provider>
  );
}

Agora vamos ao componente que consome:

// Componente que só precisa do tema
function Header() {
  const { theme, setTheme } = useContext(AppContext);

  return (
    <header style={{ background: theme === 'light' ? '#fff' : '#333' }}>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </header>
  );
}

// Componente que só precisa do usuário
function Profile() {
  const { user } = useContext(AppContext);

  return <div>User: {user?.name}</div>;
}

Quando o tema muda no Header, o Profile também sofre re-render, mesmo sem usar a informação de tema. Se você tiver 20 componentes diferentes, são 20 re-renders desnecessários.

Exemplo Prático: Depois do Splitting

// ✅ Context especializado - apenas para tema
import React, { createContext, useState } from 'react';

export const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const value = React.useMemo(
    () => ({ theme, setTheme }),
    [theme]
  );

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

// Context especializado - apenas para usuário
export const UserContext = createContext();

export function UserProvider({ children }) {
  const [user, setUser] = useState(null);

  const value = React.useMemo(
    () => ({ user, setUser }),
    [user]
  );

  return (
    <UserContext.Provider value={value}>
      {children}
    </UserContext.Provider>
  );
}

// Context especializado - apenas para notificações
export const NotificationContext = createContext();

export function NotificationProvider({ children }) {
  const [notifications, setNotifications] = useState([]);

  const value = React.useMemo(
    () => ({ notifications, setNotifications }),
    [notifications]
  );

  return (
    <NotificationContext.Provider value={value}>
      {children}
    </NotificationContext.Provider>
  );
}

Agora a estrutura no App:

function App() {
  return (
    <ThemeProvider>
      <UserProvider>
        <NotificationProvider>
          <Header />
          <Profile />
          <Sidebar />
        </NotificationProvider>
      </UserProvider>
    </ThemeProvider>
  );
}

// Header só sofre re-render quando tema muda
function Header() {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <header style={{ background: theme === 'light' ? '#fff' : '#333' }}>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </header>
  );
}

// Profile só sofre re-render quando usuário muda
function Profile() {
  const { user } = useContext(UserContext);

  return <div>User: {user?.name}</div>;
}

Agora, quando o tema muda, apenas Header sofre re-render. Profile e todos os componentes que consomem apenas UserContext ou NotificationContext permanecem intactos.

Otimizando Performance com useMemo e useCallback

Entendendo o Problema de Referência

Um erro comum é não memoizar o objeto value passado ao Provider. A cada render do Provider, um novo objeto é criado, causando re-render de todos os consumidores mesmo que os dados não tenham mudado.

// ❌ Erro - novo objeto a cada render
export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  // Novo objeto a cada render, mesmo se theme não mudou
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// ✅ Correto - objeto memoizado
export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const value = React.useMemo(
    () => ({ theme, setTheme }),
    [theme] // Só recria o objeto se theme mudar
  );

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

Callbacks e Performance

Quando você expõe funções através do Context, também deve memoizá-las com useCallback:

export function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);

  // Sem useCallback - nova função a cada render
  const login = async (email, password) => {
    setLoading(true);
    // chamada à API
    setLoading(false);
  };

  // ✅ Com useCallback - mesma função referencial
  const login = useCallback(async (email, password) => {
    setLoading(true);
    // chamada à API
    setLoading(false);
  }, []);

  const value = useMemo(
    () => ({ user, login, loading }),
    [user, loading, login]
  );

  return (
    <UserContext.Provider value={value}>
      {children}
    </UserContext.Provider>
  );
}

Separando Dados de Ações: O Padrão de Divisão Avançada

O Problema de Misturar Estado e Setters

Quando você coloca dados e suas funções de atualização no mesmo Context, consumidores que só querem ler dados ainda sofrem re-render quando as ações mudam. A solução é criar dois Contexts: um para dados e outro para ações.

// Context para dados (apenas leitura)
export const UserDataContext = createContext();

// Context para ações (apenas escrita)
export const UserActionsContext = createContext();

export function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);

  // Valores que mudam frequentemente
  const dataValue = useMemo(
    () => ({ user, loading }),
    [user, loading]
  );

  // Ações que não mudam (memoizadas com useCallback)
  const login = useCallback(async (email, password) => {
    setLoading(true);
    try {
      // chamada à API
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ email, password })
      });
      const userData = await response.json();
      setUser(userData);
    } finally {
      setLoading(false);
    }
  }, []);

  const logout = useCallback(() => {
    setUser(null);
  }, []);

  const actionsValue = useMemo(
    () => ({ login, logout }),
    [login, logout]
  );

  return (
    <UserDataContext.Provider value={dataValue}>
      <UserActionsContext.Provider value={actionsValue}>
        {children}
      </UserActionsContext.Provider>
    </UserDataContext.Provider>
  );
}

Agora você pode criar hooks customizados para abstração:

// Hook para ler dados
export function useUserData() {
  const context = useContext(UserDataContext);
  if (!context) {
    throw new Error('useUserData deve estar dentro de UserProvider');
  }
  return context;
}

// Hook para acessar ações
export function useUserActions() {
  const context = useContext(UserActionsContext);
  if (!context) {
    throw new Error('useUserActions deve estar dentro de UserProvider');
  }
  return context;
}

// Componente que só lê dados - re-render apenas quando user/loading mudam
function UserCard() {
  const { user, loading } = useUserData();

  if (loading) return <div>Carregando...</div>;
  return <div>{user?.name}</div>;
}

// Componente que precisa apenas das ações
function LoginButton() {
  const { login } = useUserActions();

  return (
    <button onClick={() => login('test@example.com', 'password')}>
      Login
    </button>
  );
}

A beleza deste padrão é que LoginButton nunca sofre re-render quando os dados de usuário mudam, porque consome apenas UserActionsContext, que tem referência estável graças ao useCallback.

Evitando Re-renders Desnecessários: Técnicas Avançadas

Selector Pattern com useContext

Quando você realmente precisa de um Context grande, use selectors para extrair apenas a parte que precisa:

// Grande Context consolidado (quando necessário)
export const AppContext = createContext();

export function AppProvider({ children }) {
  const [appState, setAppState] = useState({
    user: null,
    theme: 'light',
    notifications: [],
    settings: {}
  });

  const value = useMemo(() => appState, [appState]);

  return (
    <AppContext.Provider value={value}>
      {children}
    </AppContext.Provider>
  );
}

// ✅ Usar Selector para extrair apenas o que você precisa
export function useAppSelector(selector) {
  const context = useContext(AppContext);
  return useMemo(() => selector(context), [context, selector]);
}

// Uso
function Header() {
  // Selector função que extrai apenas tema
  const theme = useAppSelector(state => state.theme);

  return <header style={{ background: theme === 'light' ? '#fff' : '#333' }} />;
}

function Profile() {
  // Selector função que extrai apenas usuário
  const user = useAppSelector(state => state.user);

  return <div>{user?.name}</div>;
}

Atom Pattern com Composição

Para máxima granularidade, implemente um padrão de átomos similares ao Jotai:

// Sistema de átomos minimalista
const atomStore = new Map();

function createAtom(key, initialValue) {
  const context = createContext(initialValue);
  atomStore.set(key, context);
  return { context, key };
}

export const themeAtom = createAtom('theme', 'light');
export const userAtom = createAtom('user', null);

function AtomProvider({ atom, initialValue, children }) {
  const [value, setValue] = useState(initialValue);

  const atomValue = useMemo(
    () => ({ value, setValue }),
    [value]
  );

  return (
    <atom.context.Provider value={atomValue}>
      {children}
    </atom.context.Provider>
  );
}

// Uso
function App() {
  return (
    <AtomProvider atom={themeAtom} initialValue="light">
      <AtomProvider atom={userAtom} initialValue={null}>
        <Header />
        <Profile />
      </AtomProvider>
    </AtomProvider>
  );
}

// Hook para consumir átomos
function useAtom(atom) {
  const { value, setValue } = useContext(atom.context);
  return [value, setValue];
}

function Header() {
  const [theme, setTheme] = useAtom(themeAtom);
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      {theme}
    </button>
  );
}

Conclusão

Ao trabalhar com useContext em aplicações escaláveis, três princípios transformam seu código de uma bomba de re-renders para uma máquina bem-oleada. Primeiro, sempre use Context Splitting: divida grandes Contexts em especializados por domínio. Segundo, memoize agressivamente com useMemo e useCallback, garantindo que referências de objetos e funções só mudem quando realmente necessário. Terceiro, considere separar dados de ações quando seu estado é volátil, mantendo ações com referência estável enquanto dados variam. Essas três técnicas combinadas eliminam praticamente todos os re-renders desnecessários sem você precisar migrar para Redux ou Zustand.

Referências


Artigos relacionados