Study guide

State in React, shared on purpose.

Context API and Zustand, straight from the official docs: how each works, how they behave in React and Next.js, and when to pick which.

O problema que as duas resolvemThe problem both solve

Quase todo app React precisa, cedo ou tarde, que dois componentes distantes enxerguem o mesmo dado: o usuário logado, o tema, o carrinho. Esta nota usa só a documentação oficial (react.dev, Next.js e Zustand) para mostrar as duas ferramentas mais comuns para isso: a Context API, que já vem no React, e o Zustand, uma biblioteca pequena de estado. E, principalmente, quando usar cada uma.Almost every React app eventually needs two distant components to see the same data: the signed-in user, the theme, the cart. This note uses only the official documentation (react.dev, Next.js and Zustand) to show the two most common tools for that: the Context API, which ships with React, and Zustand, a small state library. And above all, when to use each one.

O problema: prop drillingThe problem: prop drilling

Props são o caminho normal para passar dados de pai para filho. Quando o dado precisa atravessar muitas camadas que não o usam, só para chegar lá embaixo, o código vira encanamento: isso é o prop drilling. A própria documentação do React manda tentar duas coisas antes de buscar contexto: passar props explicitamente (deixa o fluxo claro) e extrair componentes e passar JSX como children, o que elimina camadas que só repassam dados.Props are the normal way to pass data from parent to child. When the data has to cross many layers that do not use it just to reach the bottom, the code turns into plumbing: that is prop drilling. The React docs themselves say to try two things before reaching for context: pass props explicitly (it keeps the flow clear) and extract components and pass JSX as children, which removes layers that only forward data.

Onde o estado moraWhere the state livesQuandoWhen
useState / useReducersó um componente (ou seus filhos diretos) precisaonly one component (or its direct children) needs it
Subir o estado ("lifting up")Lifting state uppoucos irmãos precisam; props bastama few siblings need it; props are enough
Context APImuitos componentes em profundidades diferentes leem o mesmo valor, que muda pouco (tema, usuário, idioma)many components at different depths read the same value, which changes rarely (theme, user, language)
Zustandestado compartilhado que muda com frequência ou tem muitos consumidores, e você quer re-renderizar só quem usa a parte alteradashared state that changes often or has many consumers, and you want to re-render only whoever uses the changed part

Context API: o que o React já trazContext API: what React ships with

Context deixa um componente pai oferecer um valor a toda a subárvore abaixo dele, em qualquer profundidade, sem passar props camada por camada. São três passos:Context lets a parent component offer a value to the whole subtree below it, at any depth, without passing props layer by layer. There are three steps:

  • Criar o contexto com createContext(valorPadrão) e exportá-lo.Create the context with createContext(defaultValue) and export it.
  • Usar o valor em qualquer componente com useContext(MeuContexto).Use the value in any component with useContext(MyContext).
  • Prover o valor a partir de um ancestral, envolvendo os filhos em <MeuContexto value={...}>.Provide the value from an ancestor by wrapping the children in <MyContext value={...}>.
// ThemeContext.js
import { createContext, useContext, useState } from 'react';

export const ThemeContext = createContext('light');   // 1) criar (valor padrão: 'light')

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('dark');
  return (
    // 3) prover (React 19: o próprio contexto é o provider)
    <ThemeContext value={{ theme, setTheme }}>
      {children}
    </ThemeContext>
  );
}

export function useTheme() {
  return useContext(ThemeContext);                     // 2) usar
}

// Qualquer componente, em qualquer profundidade:
function ThemeButton() {
  const { theme, setTheme } = useTheme();
  return <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>{theme}</button>;
}
Regra da documentaçãoRule from the docsSignificaMeaning
O provider mais próximo venceNearest provider winsé como herança de CSS; providers aninhados sobrescrevem o valor só para a sua subárvorelike CSS inheritance; nested providers override the value only for their subtree
O valor padrão só vale sem providerThe default only applies without a providernunca muda; escolha um padrão útil ('light', não null) e os testes funcionam sem providerit never changes; pick a useful default ('light', not null) and tests work without a provider
O provider fica acimaThe provider sits aboveuseContext não enxerga o provider renderizado pelo mesmo componenteuseContext does not see a provider rendered by the same component
Componentes intermediários não importamIntermediate components do not matterpodem existir quantos quiser, até <div>; nem memo impede o consumidor de receber o valor novothere can be any number, even <div>; not even memo stops a consumer from receiving the new value
Sempre valueAlways valueescrever <Ctx theme={theme}> em vez de value= faz o consumidor receber undefinedwriting <Ctx theme={theme}> instead of value= makes consumers receive undefined
Em React 18 e anterior o provider se escrevia <MeuContexto.Provider value={...}>. Em React 19 o contexto vira o provider (<MeuContexto value={...}>); a forma antiga ainda aparece em muito código e em alguns trechos da documentação do Next.js.In React 18 and earlier the provider was written <MyContext.Provider value={...}>. In React 19 the context itself is the provider (<MyContext value={...}>); the old form still shows up in a lot of code and in some Next.js docs snippets.

A pegadinha: re-render de todos os consumidoresThe catch: every consumer re-renders

Quando o value do provider muda (comparado com Object.is), o React re-renderiza todos os componentes que leem aquele contexto. Não existe "selecionar só uma parte". E um objeto ou função escrito direto no value é novo a cada render do pai, o que dispara re-render nos consumidores mesmo sem mudança real. A documentação mostra a correção com useMemo e useCallback:When the provider's value changes (compared with Object.is), React re-renders all components that read that context. There is no "select only one part". And an object or function written straight into value is new on every parent render, which triggers consumer re-renders even with no real change. The docs show the fix with useMemo and useCallback:

function AuthProvider({ children }) {
  const [currentUser, setCurrentUser] = useState(null);

  const login = useCallback((response) => {
    storeCredentials(response.credentials);
    setCurrentUser(response.user);
  }, []);

  const contextValue = useMemo(() => ({ currentUser, login }), [currentUser, login]);

  return <AuthContext value={contextValue}>{children}</AuthContext>;
}

Context + useReducer: o padrão oficialContext + useReducer: the official pattern

Para estado com várias ações (adicionar, remover, editar), a documentação do React recomenda juntar useReducer com contexto. O padrão: dois contextos, um para ler o estado e outro para o dispatch, mais custom hooks para esconder a fiação. Quem só dispara ações não precisa re-renderizar quando o estado muda, porque o dispatch nunca muda de identidade.For state with several actions (add, remove, edit), the React docs recommend combining useReducer with context. The pattern: two contexts, one to read the state and another for dispatch, plus custom hooks to hide the wiring. Whoever only fires actions does not need to re-render when state changes, because dispatch never changes identity.

// TasksContext.js — contexto, reducer, provider e hooks no mesmo arquivo
import { createContext, useContext, useReducer } from 'react';

const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);

export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
  return (
    <TasksContext value={tasks}>
      <TasksDispatchContext value={dispatch}>
        {children}
      </TasksDispatchContext>
    </TasksContext>
  );
}

export const useTasks = () => useContext(TasksContext);
export const useTasksDispatch = () => useContext(TasksDispatchContext);

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added':   return [...tasks, { id: action.id, text: action.text, done: false }];
    case 'deleted': return tasks.filter((t) => t.id !== action.id);
    default: throw Error('Unknown action: ' + action.type);
  }
}
const initialTasks = [];

// Em qualquer lugar abaixo do provider:
const tasks = useTasks();
const dispatch = useTasksDispatch();
dispatch({ type: 'added', id: nextId++, text });

Um app pode ter vários pares assim (um para tarefas, outro para o usuário, outro para o tema). Separar contextos por assunto também limita o estrago do re-render: mudar o tema não acorda quem só lê tarefas.An app can have several such pairs (one for tasks, another for the user, another for the theme). Splitting contexts by topic also limits the re-render cost: changing the theme does not wake up whoever only reads tasks.

Context no Next.js (App Router)Context in Next.js (App Router)

O App Router do Next.js faz de Server Components o padrão para layouts e páginas. Eles buscam dados, usam segredos e não enviam JavaScript ao navegador, mas não suportam contexto nem hooks. Ou seja: o contexto vive só nos Client Components (arquivos com 'use client').The Next.js App Router makes Server Components the default for layouts and pages. They fetch data, use secrets and ship no JavaScript to the browser, but they do not support context or hooks. In other words: context lives only in Client Components (files with 'use client').

A receita oficial: criar um Client Component que recebe children e importá-lo no layout, que continua sendo um Server Component.The official recipe: create a Client Component that accepts children and import it in the layout, which stays a Server Component.

// app/theme-provider.tsx
'use client';
import { createContext } from 'react';

export const ThemeContext = createContext({});

export default function ThemeProvider({ children }: { children: React.ReactNode }) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}

// app/layout.tsx  (Server Component)
import ThemeProvider from './theme-provider';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}
  • Provider o mais fundo possível. Note que o ThemeProvider envolve só {children}, não o <html> inteiro: assim o Next.js consegue otimizar as partes estáticas dos Server Components.Providers as deep as possible. Note that ThemeProvider wraps only {children}, not the whole <html>: that lets Next.js optimize the static parts of your Server Components.
  • 'use client' é uma fronteira. Tudo que o arquivo importa e renderiza diretamente entra no bundle do cliente; Server Components passados como children continuam no servidor.'use client' is a boundary. Everything the file imports and renders directly goes into the client bundle; Server Components passed as children stay on the server.
  • Dados do servidor chegam por props. Um Server Component busca o dado e o passa a um Client Component como prop (precisa ser serializável); só então ele pode ir para um contexto ou store.Server data arrives via props. A Server Component fetches the data and passes it to a Client Component as a prop (it must be serializable); only then can it go into a context or store.

Zustand: store, seletores e açõesZustand: store, selectors and actions

O Zustand é uma biblioteca pequena de estado com API de hooks, pouca cerimônia e sem provider no uso básico. O store é um hook criado com create; ele guarda dados e as ações que os alteram.Zustand is a small state library with a hooks API, little ceremony and no provider in basic use. The store is a hook created with create; it holds data and the actions that change it.

npm install zustand
import { create } from 'zustand';

const useBear = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}));

function BearCounter() {
  const bears = useBear((state) => state.bears);       // seletor: pega só o que usa
  return <h1>{bears} bears around here...</h1>;
}

function Controls() {
  const increasePopulation = useBear((state) => state.increasePopulation);
  return <button onClick={increasePopulation}>one up</button>;
}

Seletores: a diferença que importaSelectors: the difference that matters

Cada componente passa ao hook um seletor, uma função que escolhe um pedaço do estado. O componente só re-renderiza quando o resultado do seletor muda (comparado com Object.is). No exemplo, o BearCounter atualiza quando bears muda; o Controls lê só a função e quase nunca re-renderiza.Each component hands the hook a selector, a function that picks a slice of the state. The component re-renders only when the selector's result changes (compared with Object.is). In the example, BearCounter updates when bears changes; Controls reads only the function and almost never re-renders.

Cuidado com seletores que devolvem um valor novo a cada chamada (como Object.keys(state), que cria um array novo): eles re-renderizam a cada mudança do store. A correção oficial é embrulhar o seletor em useShallow, que compara o resultado de forma rasa:Beware of selectors that return a new value on every call (like Object.keys(state), which builds a new array): they re-render on every store change. The official fix is to wrap the selector in useShallow, which compares the result shallowly:

import { useShallow } from 'zustand/react/shallow';

const names = useMeals(useShallow((state) => Object.keys(state)));

Atualizando o estadoUpdating state

  • set faz mesclagem rasa: passe só as chaves que mudam; as outras do primeiro nível continuam.set merges shallowly: pass only the keys that change; the other first-level keys stay.
  • Objetos aninhados são imutáveis: copie cada nível com spread (...state.deep) ou use o middleware Immer para escrever em estilo "mutável" dentro de produce.Nested objects are immutable: copy each level with spread (...state.deep) or use the Immer middleware to write in a "mutable" style inside produce.
  • Em TypeScript use a forma curried create<Estado>()((set) => ...) para tipar o store.In TypeScript use the curried form create<State>()((set) => ...) to type the store.
set((state) => ({
  deep: {
    ...state.deep,
    nested: { ...state.deep.nested, count: state.deep.nested.count + 1 },
  },
}));

Zustand no Next.js: um store por requisiçãoZustand in Next.js: one store per request

Aqui está a pegadinha central. Um store criado com create é estado de módulo, global. Um servidor Next.js atende muitas requisições ao mesmo tempo; se o store for global, o estado de um usuário pode vazar para a renderização de outro. Por isso o guia oficial do Zustand para Next.js manda criar o store por requisição, com uma fábrica, e distribuí-lo por contexto.Here is the central catch. A store created with create is module state, global. A Next.js server handles many requests at once; if the store is global, one user's state can leak into another's render. That is why the official Zustand guide for Next.js says to create the store per request, with a factory, and distribute it through context.

// src/stores/counter-store.ts — a fábrica (zustand/vanilla)
import { createStore } from 'zustand/vanilla';

export type CounterState = { count: number };
export type CounterActions = { incrementCount: () => void; decrementCount: () => void };
export type CounterStore = CounterState & CounterActions;

export const defaultInitState: CounterState = { count: 0 };

export const createCounterStore = (initState: CounterState = defaultInitState) =>
  createStore<CounterStore>()((set) => ({
    ...initState,
    incrementCount: () => set((s) => ({ count: s.count + 1 })),
    decrementCount: () => set((s) => ({ count: s.count - 1 })),
  }));
// src/providers/counter-store-provider.tsx
'use client';
import { type ReactNode, createContext, useState, useContext } from 'react';
import { useStore } from 'zustand';
import { type CounterStore, createCounterStore } from '@/stores/counter-store';

export type CounterStoreApi = ReturnType<typeof createCounterStore>;
const CounterStoreContext = createContext<CounterStoreApi | undefined>(undefined);

export const CounterStoreProvider = ({ children }: { children: ReactNode }) => {
  const [store] = useState(() => createCounterStore());   // criado uma vez por instância
  return <CounterStoreContext.Provider value={store}>{children}</CounterStoreContext.Provider>;
};

export const useCounterStore = <T,>(selector: (s: CounterStore) => T): T => {
  const ctx = useContext(CounterStoreContext);
  if (!ctx) throw new Error('useCounterStore must be used within CounterStoreProvider');
  return useStore(ctx, selector);
};
// src/app/layout.tsx — App Router (no Pages Router: dentro do _app)
import { CounterStoreProvider } from '@/providers/counter-store-provider';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <CounterStoreProvider>{children}</CounterStoreProvider>
      </body>
    </html>
  );
}

// src/components/home-page.tsx
'use client';
import { useCounterStore } from '@/providers/counter-store-provider';

export const HomePage = () => {
  const count = useCounterStore((s) => s.count);
  const increment = useCounterStore((s) => s.incrementCount);
  return <button onClick={increment}>Count: {count}</button>;
};
Repare: aqui o Zustand usa Context só como encanamento para entregar a instância do store. O re-render seletivo continua vindo dos seletores. A consequência prática é que o contexto distribui onde está o store, mas não o faz re-renderizar todo mundo (a referência do store nunca muda).Note: here Zustand uses Context only as plumbing to deliver the store instance. Selective re-rendering still comes from selectors. The practical result is that context distributes where the store is without re-rendering everyone (the store reference never changes).
  • Server Components não leem nem escrevem no store. Eles não usam hooks nem contexto; busque dados no servidor e passe-os por props.Server Components neither read nor write the store. They cannot use hooks or context; fetch data on the server and pass it down as props.
  • Hidratação: o Next.js renderiza no servidor e de novo no cliente; se os dois resultados diferem, há erro de hidratação. Inicialize o store no servidor com dados e reinicialize no cliente com os mesmos dados (a fábrica aceita um initState para isso). Valores do navegador, como localStorage ou window.matchMedia, só devem ser lidos depois da hidratação.Hydration: Next.js renders on the server and again on the client; if the two outputs differ you get a hydration error. Initialise the store on the server with data and re-initialise on the client with the same data (the factory accepts an initState for that). Browser-only values such as localStorage or window.matchMedia should be read only after hydration.
  • Um store por rota é possível (provider dentro da página), mas o guia desaconselha, a menos que cada rota precise de um store separado.One store per route is possible (provider inside the page), but the guide advises against it unless each route truly needs a separate store.

Context ou Zustand? ComparandoContext or Zustand? Comparing

Context APIContext APIZustandZustand
Vem de ondeWhere it comes fromReact (nada a instalar)React (nothing to install)biblioteca separadaseparate library
ProviderProviderobrigatóriorequirednão, no uso básico; no Next.js, sim (store por requisição)not in basic use; in Next.js, yes (per-request store)
Re-renderRe-rendertodo consumidor quando o value mudaevery consumer when value changessó quem usa o pedaço mudado (seletores)only whoever uses the changed slice (selectors)
Onde fica a lógicaWhere logic livesem useState/useReducer do providerin the provider's useState/useReducerdentro do store (dados + ações juntos)inside the store (data + actions together)
Uso fora de componentesUse outside componentsnãonosim (o store é um objeto com getState)yes (the store is an object with getState)
Melhor paraBest forvalores estáveis e injeção de dependência: tema, idioma, usuáriostable values and dependency injection: theme, language, userestado de cliente dinâmico: carrinho, filtros, UI complexa, editoresdynamic client state: cart, filters, complex UI, editors
Context: o valor muda e TODOS os consumidores re-renderizamContext: the value changes and ALL consumers re-render Provider A ↻ B ↻ C ↻ Zustand: só o componente cujo seletor mudou re-renderizaZustand: only the component whose selector changed re-renders store A ↻ B C ↻ = re-renderiza. Linhas pontilhadas: conectados ao store, mas o seletor deles não mudou.↻ = re-renders. Dashed lines: connected to the store, but their selector did not change.
A diferença central: Context avisa todos os consumidores; Zustand compara o resultado de cada seletor.The core difference: Context notifies every consumer; Zustand compares each selector's result.

Como decidirHow to decide

  • Comece pelo mais simples: estado local, depois subir o estado, depois children. Só então contexto.Start with the simplest: local state, then lifting state up, then children. Only then context.
  • Contexto para o que muda pouco e muitos leem: tema, idioma, usuário autenticado, configuração.Context for what rarely changes and many read: theme, language, authenticated user, configuration.
  • Zustand quando o estado muda com frequência, tem muitos consumidores independentes, ou a lógica fica grande demais para um provider.Zustand when state changes often, has many independent consumers, or the logic grows too big for a provider.
  • Dados que vêm do servidor (listas, perfis) não são "estado global de UI": busque-os em Server Components e passe por props. Guarde no store só o que é estado de interface.Data that comes from the server (lists, profiles) is not "global UI state": fetch it in Server Components and pass it down as props. Keep only interface state in the store.
  • Os dois combinam: Context para tema e usuário, Zustand para o carrinho.They combine: Context for theme and user, Zustand for the cart.

Exercício: tema e contadorExercise: theme and counter

Um app Next.js pequeno com os dois: tema por Context, contador por Zustand (store por requisição). Pré-requisito: Node.js instalado.A small Next.js app with both: theme through Context, counter through Zustand (per-request store). Prerequisite: Node.js installed.

npx create-next-app@latest lab-estado --ts --app
cd lab-estado
npm install zustand
mkdir -p src/stores src/providers

Crie os arquivos src/stores/counter-store.ts e src/providers/counter-store-provider.tsx da seção anterior, e um src/providers/theme-provider.tsx ('use client') com o ThemeProvider e o useTheme da seção de Context. Depois envolva o {children} do layout.tsx nos dois providers e crie um componente cliente que use os hooks.Create the files src/stores/counter-store.ts and src/providers/counter-store-provider.tsx from the previous section, and a src/providers/theme-provider.tsx ('use client') with the ThemeProvider and useTheme from the Context section. Then wrap the {children} of layout.tsx in both providers and create a client component that uses the hooks.

npm run dev      # http://localhost:3000

Resultado esperado: o botão do tema alterna o texto e o contador sobe ao clicar. Para ver a diferença de re-render, instale a extensão React Developer Tools, ative "Highlight updates when components render" e clique no contador: só os componentes que leem o count piscam. Depois troque o seletor por useCounterStore((s) => s) (o store inteiro) e veja mais componentes piscarem.Expected result: the theme button toggles the text and the counter goes up on click. To see the re-render difference, install the React Developer Tools extension, enable "Highlight updates when components render" and click the counter: only the components that read count flash. Then change the selector to useCounterStore((s) => s) (the whole store) and watch more components flash.

Cola e revisãoCheat sheet and review

ColaCheat sheet

Quero…I want…UsoI use
passar um valor para toda a subárvorepass a value to the whole subtreecreateContext + useContext + <Ctx value>
estado com várias ações via contextostate with several actions via contextuseReducer + 2 contextos + custom hooks
contexto no App Routercontext in the App RouterClient Component com 'use client' recebendo children, importado no layoutClient Component with 'use client' accepting children, imported in the layout
um store simplesa simple storecreate((set) => ({...}))
re-renderizar só o necessáriore-render only what is neededseletor, e useShallow se ele devolve objeto/array novoa selector, and useShallow if it returns a new object/array
Zustand com Next.jsZustand with Next.jscreateStore + provider 'use client' com useState(() => ...)createStore + 'use client' provider with useState(() => ...)

Perguntas de revisãoReview questions

  • O que é prop drilling e o que a documentação do React manda tentar antes do contexto?What is prop drilling and what do the React docs say to try before context?
  • Por que um objeto escrito direto em value causa re-renders extras, e como corrigir?Why does an object written straight into value cause extra re-renders, and how do you fix it?
  • Por que o padrão reducer + contexto usa dois contextos?Why does the reducer + context pattern use two contexts?
  • Por que Server Components não usam contexto, e onde o provider deve ficar?Why can Server Components not use context, and where should the provider live?
  • O que um seletor do Zustand faz e quando o useShallow entra?What does a Zustand selector do and when does useShallow come in?
  • Por que um store global é um risco num servidor Next.js, e qual é a correção oficial?Why is a global store a risk on a Next.js server, and what is the official fix?