Aprenda / Frontend — Tailwind & Next.js

IntermediárioAula 11 de 2016 min0% da trilha

Estado global: Context API vs. Zustand

Prop drilling incomoda, mas a resposta nem sempre é Context — às vezes é uma lib pequena que não re-renderiza a árvore inteira.

Prop drilling — passar uma prop por 3, 4, 5 componentes só pra ela chegar lá no fundo, sem que nenhum componente no meio do caminho use ela — é chato, mas não é sempre um problema de verdade. Vira problema quando o valor muda com frequência, é usado em pontos bem distantes da árvore, ou quando adicionar mais um nível de aninhamento significa tocar em componentes que não têm nada a ver com aquele dado.

Prop drilling — o dado passa por quem não usa
function App() {
  const [usuario, setUsuario] = useState<Usuario | null>(null);
  return <Layout usuario={usuario} />;
}

function Layout({ usuario }: { usuario: Usuario | null }) {
  return <Header usuario={usuario} />; // Layout não usa "usuario", só repassa
}

function Header({ usuario }: { usuario: Usuario | null }) {
  return <MenuDoUsuario usuario={usuario} />; // Header também só repassa
}

Context API — built-in, mas com um custo real

O Context resolve o repasse: qualquer componente dentro do Provider lê o valor direto com useContext, sem prop nenhuma no meio do caminho.

theme-context.tsx
const ThemeContext = createContext<"light" | "dark">("light");

function App() {
  const [tema, setTema] = useState<"light" | "dark">("light");
  return (
    <ThemeContext.Provider value={tema}>
      <Layout />
    </ThemeContext.Provider>
  );
}

function BotaoTema() {
  const tema = useContext(ThemeContext); // sem prop nenhuma
  return <button className={tema === "dark" ? "bg-slate-800" : "bg-white"}>Alternar tema</button>;
}

O detalhe que pega muita gente de surpresa: quando o valor do Context muda, todo componente que consome aquele Context via useContext re-renderiza — mesmo que ele só use uma parte pequena do valor, ou que o valor seja um objeto grande com vários campos independentes. Pra um tema que muda raramente, isso não importa. Pra um estado que muda a cada tecla digitada, importa muito.

Zustand — estado que muda com frequência sem re-renderizar tudo

Zustand é uma lib pequena e sem boilerplate de Provider: você cria uma store fora da árvore de componentes, e cada componente escolhe, via um seletor, só a fatia do estado que precisa.

carrinho-store.ts
import { create } from "zustand";

type CarrinhoState = {
  itens: { id: string; nome: string }[];
  adicionar: (item: { id: string; nome: string }) => void;
};

export const useCarrinhoStore = create<CarrinhoState>((set) => ({
  itens: [],
  adicionar: (item) => set((state) => ({ itens: [...state.itens, item] })),
}));

// Um componente que só mostra a quantidade só re-renderiza quando "itens.length" muda:
function ContadorDoCarrinho() {
  const quantidade = useCarrinhoStore((state) => state.itens.length);
  return <span>{quantidade} itens</span>;
}

Repare no seletor (state) => state.itens.length: o componente só re-renderiza quando aquele valor específico muda, não sempre que qualquer coisa na store muda. É essa diferença que faz Zustand escalar melhor pra estado que muda com frequência.

Quando usar qual

Context encaixa bem em valores que mudam raramente e são majoritariamente lidos — tema, idioma, o usuário autenticado. Zustand (ou lib parecida) encaixa melhor em estado que muda com frequência e onde componentes diferentes precisam de fatias diferentes dele — filtros de uma tabela, carrinho de compras, passos de um wizard.

Exercício

Reescreva o ContadorDoCarrinho acima como se ele lesse de um ThemeContext-like Context em vez de Zustand. Depois explique, em uma frase, por que essa versão re-renderiza mais vezes que a versão com seletor do Zustand.

Quiz rápido

Qual é uma limitação real do Context API que motiva usar Zustand em estado que muda com frequência?
Por que uma store Zustand normalmente causa menos re-renders desnecessários que Context num estado que muda bastante?