React & Frontend

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

10 min de leitura

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

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 ,

<h2>O Problema Real do useContext em Aplicações Escaláveis</h2>

<p>Quando iniciamos com React e descobrimos o <code>useContext</code>, parece uma solução mágica para evitar prop drilling. No entanto, conforme sua aplicação cresce, você descobrirá um problema fundamental: o <code>useContext</code> causa re-renders em <strong>todos os componentes que o consomem</strong>, 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.</p>

<p>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.</p>

<h2>Context Splitting: Dividindo para Vencer</h2>

<h3>O Conceito Fundamental</h3>

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

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

<h3>Exemplo Prático: Antes do Splitting</h3>

<pre><code class="language-javascript">// ❌ Contexto monolítico - problema de performance

import React, { createContext, useState } from &#039;react&#039;;

export const AppContext = createContext();

export function AppProvider({ children }) {

const [user, setUser] = useState(null);

const [theme, setTheme] = useState(&#039;light&#039;);

const [notifications, setNotifications] = useState([]);

const [settings, setSettings] = useState({});

return (

&lt;AppContext.Provider value={{ user, setUser, theme, setTheme, notifications, setNotifications, settings, setSettings }}&gt;

{children}

&lt;/AppContext.Provider&gt;

);

}</code></pre>

<p>Agora vamos ao componente que consome:</p>

<pre><code class="language-javascript">// Componente que só precisa do tema

function Header() {

const { theme, setTheme } = useContext(AppContext);

return (

&lt;header style={{ background: theme === &#039;light&#039; ? &#039;#fff&#039; : &#039;#333&#039; }}&gt;

&lt;button onClick={() =&gt; setTheme(theme === &#039;light&#039; ? &#039;dark&#039; : &#039;light&#039;)}&gt;

Toggle Theme

&lt;/button&gt;

&lt;/header&gt;

);

}

// Componente que só precisa do usuário

function Profile() {

const { user } = useContext(AppContext);

return &lt;div&gt;User: {user?.name}&lt;/div&gt;;

}</code></pre>

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

<h3>Exemplo Prático: Depois do Splitting</h3>

<pre><code class="language-javascript"></code></pre>

<p>Agora a estrutura no App:</p>

<pre><code class="language-javascript">function App() {

return (

&lt;ThemeProvider&gt;

&lt;UserProvider&gt;

&lt;NotificationProvider&gt;

&lt;Header /&gt;

&lt;Profile /&gt;

&lt;Sidebar /&gt;

&lt;/NotificationProvider&gt;

&lt;/UserProvider&gt;

&lt;/ThemeProvider&gt;

);

}

// Header só sofre re-render quando tema muda

function Header() {

const { theme, setTheme } = useContext(ThemeContext);

return (

&lt;header style={{ background: theme === &#039;light&#039; ? &#039;#fff&#039; : &#039;#333&#039; }}&gt;

&lt;button onClick={() =&gt; setTheme(theme === &#039;light&#039; ? &#039;dark&#039; : &#039;light&#039;)}&gt;

Toggle Theme

&lt;/button&gt;

&lt;/header&gt;

);

}

// Profile só sofre re-render quando usuário muda

function Profile() {

const { user } = useContext(UserContext);

return &lt;div&gt;User: {user?.name}&lt;/div&gt;;

}</code></pre>

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

<h2>Otimizando Performance com useMemo e useCallback</h2>

<h3>Entendendo o Problema de Referência</h3>

<p>Um erro comum é não memoizar o objeto <code>value</code> 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.</p>

<pre><code class="language-javascript"></code></pre>

<h3>Callbacks e Performance</h3>

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

<pre><code class="language-javascript"></code></pre>

<h2>Separando Dados de Ações: O Padrão de Divisão Avançada</h2>

<h3>O Problema de Misturar Estado e Setters</h3>

<p>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.</p>

<pre><code class="language-javascript">// 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(

() =&gt; ({ user, loading }),

[user, loading]

);

// Ações que não mudam (memoizadas com useCallback)

const login = useCallback(async (email, password) =&gt; {

setLoading(true);

try {

// chamada à API

const response = await fetch(&#039;/api/login&#039;, {

method: &#039;POST&#039;,

body: JSON.stringify({ email, password })

});

const userData = await response.json();

setUser(userData);

} finally {

setLoading(false);

}

}, []);

const logout = useCallback(() =&gt; {

setUser(null);

}, []);

const actionsValue = useMemo(

() =&gt; ({ login, logout }),

[login, logout]

);

return (

&lt;UserDataContext.Provider value={dataValue}&gt;

&lt;UserActionsContext.Provider value={actionsValue}&gt;

{children}

&lt;/UserActionsContext.Provider&gt;

&lt;/UserDataContext.Provider&gt;

);

}</code></pre>

<p>Agora você pode criar hooks customizados para abstração:</p>

<pre><code class="language-javascript">// Hook para ler dados

export function useUserData() {

const context = useContext(UserDataContext);

if (!context) {

throw new Error(&#039;useUserData deve estar dentro de UserProvider&#039;);

}

return context;

}

// Hook para acessar ações

export function useUserActions() {

const context = useContext(UserActionsContext);

if (!context) {

throw new Error(&#039;useUserActions deve estar dentro de UserProvider&#039;);

}

return context;

}

// Componente que só lê dados - re-render apenas quando user/loading mudam

function UserCard() {

const { user, loading } = useUserData();

if (loading) return &lt;div&gt;Carregando...&lt;/div&gt;;

return &lt;div&gt;{user?.name}&lt;/div&gt;;

}

// Componente que precisa apenas das ações

function LoginButton() {

const { login } = useUserActions();

return (

&lt;button onClick={() =&gt; login(&#039;test@example.com&#039;, &#039;password&#039;)}&gt;

Login

&lt;/button&gt;

);

}</code></pre>

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

<h2>Evitando Re-renders Desnecessários: Técnicas Avançadas</h2>

<h3>Selector Pattern com useContext</h3>

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

<pre><code class="language-javascript"></code></pre>

<h3>Atom Pattern com Composição</h3>

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

<pre><code class="language-javascript">// 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(&#039;theme&#039;, &#039;light&#039;);

export const userAtom = createAtom(&#039;user&#039;, null);

function AtomProvider({ atom, initialValue, children }) {

const [value, setValue] = useState(initialValue);

const atomValue = useMemo(

() =&gt; ({ value, setValue }),

[value]

);

return (

&lt;atom.context.Provider value={atomValue}&gt;

{children}

&lt;/atom.context.Provider&gt;

);

}

// Uso

function App() {

return (

&lt;AtomProvider atom={themeAtom} initialValue=&quot;light&quot;&gt;

&lt;AtomProvider atom={userAtom} initialValue={null}&gt;

&lt;Header /&gt;

&lt;Profile /&gt;

&lt;/AtomProvider&gt;

&lt;/AtomProvider&gt;

);

}

// Hook para consumir átomos

function useAtom(atom) {

const { value, setValue } = useContext(atom.context);

return [value, setValue];

}

function Header() {

const [theme, setTheme] = useAtom(themeAtom);

return (

&lt;button onClick={() =&gt; setTheme(theme === &#039;light&#039; ? &#039;dark&#039; : &#039;light&#039;)}&gt;

{theme}

&lt;/button&gt;

);

}</code></pre>

<h2>Conclusão</h2>

<p>Ao trabalhar com <code>useContext</code> 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, <strong>sempre use Context Splitting</strong>: divida grandes Contexts em especializados por domínio. Segundo, <strong>memoize agressivamente</strong> com <code>useMemo</code> e <code>useCallback</code>, garantindo que referências de objetos e funções só mudem quando realmente necessário. Terceiro, <strong>considere separar dados de ações</strong> 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.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://react.dev/reference/react/useContext" target="_blank" rel="noopener noreferrer">React Documentation: useContext</a></li>

<li><a href="https://react.dev/reference/react/useMemo" target="_blank" rel="noopener noreferrer">React Documentation: useMemo</a></li>

<li><a href="https://kentcdodds.com/blog/how-to-use-react-context-effectively" target="_blank" rel="noopener noreferrer">Kent C. Dodds - How to use React Context effectively</a></li>

<li><a href="https://web.dev/react/" target="_blank" rel="noopener noreferrer">Web Dev: React Context API Performance</a></li>

<li><a href="https://jotai.org/" target="_blank" rel="noopener noreferrer">Jotai - Primitive and Flexible State Management</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

Arquitetura de Frontend em Escala: Decisões, Trade-offs e Evolução: Do Básico ao Avançado
Arquitetura de Frontend em Escala: Decisões, Trade-offs e Evolução: Do Básico ao Avançado

Fundamentos de Arquitetura Frontend em Escala A arquitetura de frontend em es...

Boas Práticas de Compound Components em React: API Flexível e Contexto Implícito para Times Ágeis
Boas Práticas de Compound Components em React: API Flexível e Contexto Implícito para Times Ágeis

O Padrão Compound Components O padrão Compound Components é uma arquitetura d...

Guia Completo de React Query Avançado: Cache, Stale Time, Prefetch e Optimistic UI
Guia Completo de React Query Avançado: Cache, Stale Time, Prefetch e Optimistic UI

Entendendo o Cache e sua Importância no React Query O cache é o coração do Re...