Um projeto educacional demonstrando como criar um gerenciador de estado moderno usando o padrão Observable, similar ao Zustand, mas construído do zero para fins didáticos.
- Sobre o Projeto
- O Problema com Context API
- Introdução ao Padrão Observable
- Arquitetura da Solução
- Implementação Detalhada
- Guia de Uso
- Comparação: Context API vs Este Gerenciador
- Perguntas e Respostas
- Como Executar
- Recursos Adicionais
Este repositório contém uma implementação educacional de um gerenciador de estado global para React, inspirado no Zustand, mas construído do zero para demonstrar:
- ✅ Como funciona o padrão Observer/Observable
- ✅ Por que gerenciadores externos são mais performáticos que Context API
- ✅ Como o React se integra com stores externos via
useSyncExternalStore - ✅ Técnicas de otimização com seletores granulares
Ao estudar este projeto, você vai entender:
- Os problemas do Context API em aplicações grandes
- Como implementar um gerenciador de estado do zero
- Por que o padrão Observable evita re-renders desnecessários
- Como usar seletores para otimizar performance
- Quando escolher entre Context API e gerenciadores externos
- React 18+ - useSyncExternalStore para integração com stores externas
- TypeScript - Type-safety completo
- Vite - Build tool rápido com Hot Module Replacement
- Tailwind CSS - Estilização utility-first
Quando usamos Context API, todos os componentes que consomem o contexto são re-renderizados, mesmo que não utilizem os dados que foram atualizados.
// ❌ PROBLEMA: Todos os consumidores renderizam juntos
const AppContext = createContext(null);
function App() {
const [user, setUser] = useState(null);
const [todos, setTodos] = useState([]);
return (
<AppContext.Provider value={{ user, todos, setUser, setTodos }}>
<UserMenu /> {/* Re-renderiza quando todos mudam */}
<TodosList /> {/* Re-renderiza quando user muda */}
<TodosCounter /> {/* Re-renderiza quando user muda */}
</AppContext.Provider>
);
}
function UserMenu() {
const { user, login, logout } = useContext(AppContext);
// ⚠️ Este componente re-renderiza toda vez que 'todos' muda!
// Mesmo usando apenas 'user'
}- Provider único: Todo o estado fica em um único objeto de contexto
- Sem granularidade: React não consegue saber qual parte do contexto cada componente realmente usa
- Reconciliação conservadora: Por segurança, React re-renderiza todos os consumidores quando o contexto muda
User faz login
↓
Contexto muda { user: {...}, todos: [...] }
↓
React detecta mudança no Provider
↓
├─→ UserMenu re-renderiza ✓ (precisa do user)
├─→ TodosList re-renderiza ✗ (não usa user)
└─→ TodosCounter re-renderiza ✗ (não usa user)
- 📉 Performance degradada em apps com muitos componentes
- 🔄 Renderizações desnecessárias consomem CPU
- 🐛 Bugs sutis de performance difíceis de debugar
- 🔧 Necessidade de otimizações manuais (múltiplos contexts, memoization complexa)
// ⚠️ Solução 1: Múltiplos Contexts (verboso)
<UserContext.Provider value={user}>
<TodosContext.Provider value={todos}>
<App />
</TodosContext.Provider>
</UserContext.Provider>
// ⚠️ Solução 2: Memoization (complexo e frágil)
const MemoizedUserMenu = memo(UserMenu);
const MemoizedTodosList = memo(TodosList);Essas soluções funcionam, mas adicionam complexidade e boilerplate. É aqui que gerenciadores de estado externos brilham!
O padrão Observable (também conhecido como Observer ou Pub/Sub) é um padrão de design onde um objeto observável (Subject) mantém uma lista de observadores (Observers) e os notifica automaticamente sobre mudanças de estado.
Imagine um canal do YouTube:
- 📺 Canal (Observable): Contém vídeos (estado)
- 👥 Inscritos (Observers): Pessoas que querem ser notificadas
- 🔔 Notificação: Quando um vídeo novo é publicado, apenas os inscritos recebem
┌─────────────────────────────────────────┐
│ CANAL DO YOUTUBE (Subject/Observable) │
│ Estado: [video1, video2, video3] │
│ Inscritos: Set { user1, user2, user3 } │
└─────────────────┬───────────────────────┘
│
│ 🎬 Novo vídeo publicado!
│
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ user1 │ │ user2 │ │ user3 │
│ 🔔 Sim! │ │ 🔔 Sim! │ │ 🔔 Sim! │
└─────────┘ └─────────┘ └─────────┘
┌─────────┐
│ user4 │ ← Não inscrito, NÃO recebe notificação
│ 🔕 Não │
└─────────┘
Conexão com nosso gerenciador:
- Canal = Store (createStore)
- Vídeos = Estado (user, todos)
- Inscritos = Componentes React
- Notificação = Re-render
O objeto que mantém o estado e a lista de observers.
let state = { user: null, todos: [] }; // Estado
let listeners = new Set(); // ObservadoresFunções/objetos que reagem às mudanças.
function componentListener() {
console.log('Estado mudou! Preciso re-renderizar');
}Método para registrar um observador.
function subscribe(listener) {
listeners.add(listener); // Adiciona à lista
return () => { // Retorna unsubscribe
listeners.delete(listener);
};
}Método que avisa todos os observadores.
function notifyListeners() {
listeners.forEach(listener => listener());
}// ❌ Com Array - permite duplicados
const listeners = [];
listeners.push(callback);
listeners.push(callback); // Adicionado novamente!
console.log(listeners); // [callback, callback] ← PROBLEMA!
// Resultado: callback executa 2x quando notificar
// ✅ Com Set - evita duplicados automaticamente
const listeners = new Set();
listeners.add(callback);
listeners.add(callback); // Ignorado automaticamente
console.log(listeners); // Set { callback } ← SEGURO!
// Resultado: callback executa apenas 1xVantagens do Set:
- ✅ Evita duplicação automática
- ✅ Operações
addedeletesão O(1) - muito rápidas - ✅ Não precisa verificar se já existe antes de adicionar
- ✅ Código mais limpo e seguro
┌──────────────────────────────────────────────────┐
│ 1️⃣ SUBSCRIBE │
│ Component → subscribe(listener) → Set.add() │
└──────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────┐
│ 2️⃣ USER ACTION │
│ Click button → calls login() │
└──────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────┐
│ 3️⃣ UPDATE STATE │
│ setState({ user: {...} }) │
│ state = { ...state, ...newValue } │
└──────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────┐
│ 4️⃣ NOTIFY │
│ notifyListeners() → listeners.forEach(fn => fn())│
└──────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────┐
│ 5️⃣ SELECTOR CHECK │
│ useSyncExternalStore executa selector │
│ newValue = selector(state) │
│ if (newValue !== oldValue) → RE-RENDER │
│ if (newValue === oldValue) → SKIP ✓ │
└──────────────────────────────────────────────────┘
// Estado inicial
let state = { count: 0 };
let listeners = new Set();
// Componente se inscreve
const listener = () => console.log('Re-renderizando!');
listeners.add(listener);
// User clica no botão
function increment() {
state = { count: state.count + 1 }; // Atualiza
listeners.forEach(fn => fn()); // Notifica
}
increment();
// Console: "Re-renderizando!"Nossa implementação consiste em três camadas bem definidas:
┌────────────────────────────────────────────────────┐
│ CAMADA 1: createStore (Factory Genérica) │
│ │
│ - Cria stores Observable │
│ - Genérico e reutilizável (TypeScript) │
│ - Fornece: setState, subscribe, getState, useStore│
│ - Integração com React via useSyncExternalStore │
└──────────────────────┬─────────────────────────────┘
│
│ createStore<IGlobalStore>(...)
│
▼
┌────────────────────────────────────────────────────┐
│ CAMADA 2: globalStore (Instância Específica) │
│ │
│ - Store específica da aplicação │
│ - Define estado: user, todos │
│ - Define ações: login, logout, addTodo, etc │
│ - Exporta: useGlobalStore │
└──────────────────────┬─────────────────────────────┘
│
│ import useGlobalStore
│
▼
┌────────────────────────────────────────────────────┐
│ CAMADA 3: Components (Consumidores) │
│ │
│ - UserMenu → seleciona state.user │
│ - TodosList → seleciona state.todos │
│ - TodosCounter → seleciona state.todos.length │
│ - TodoForm → seleciona apenas ações │
└────────────────────────────────────────────────────┘
src/
├── store/
│ ├── createStore.ts ← 🏭 Factory genérica (reutilizável)
│ │ Implementa Observable Pattern
│ │ Exporta função createStore<T>
│ │
│ └── globalStore.ts ← 🗄️ Store da aplicação
│ Define IGlobalStore interface
│ Cria instância com createStore
│ Exporta useGlobalStore hook
│
├── components/
│ ├── UserMenu.tsx ← Consome state.user
│ ├── TodosList.tsx ← Consome state.todos + actions
│ ├── TodosCounter.tsx ← Consome state.todos (length)
│ └── TodoForm.tsx ← Consome apenas actions (memo)
│
├── entities/
│ ├── IUser.ts ← Interface: { name, email }
│ └── ITodo.ts ← Interface: { id, title, author, done }
│
├── hooks/
│ └── useRenderCounter.ts ← 🐛 Debug: conta re-renders
│
└── utils/
└── cn.ts ← Utility: merge classNames
┌─────────────────────────────────────────────────┐
│ 1️⃣ USER INTERACTION │
│ │
│ User clica em "Entrar" │
└──────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 2️⃣ COMPONENT CALLS ACTION │
│ │
│ const login = useGlobalStore(s => s.login) │
│ login() // Executa a ação │
└──────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 3️⃣ ACTION CALLS setState │
│ │
│ login: () => setState({ user: {...} }) │
└──────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 4️⃣ setState UPDATES INTERNAL STATE │
│ │
│ state = { ...state, user: {...} } │
│ // user mudou, todos permanece igual │
└──────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 5️⃣ setState CALLS notifyListeners │
│ │
│ listeners.forEach(listener => listener()) │
│ // TODOS os componentes são notificados │
└──────────────┬──────────────────────────────────┘
│
├───────────────┬───────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│UserMenu │ │TodosList │ │TodosCount│
│ │ │ │ │er │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────┐
│ 6️⃣ useSyncExternalStore RUNS SELECTOR │
│ │
│ UserMenu: selector = s => s.user │
│ Antes: null → Agora: {name, email} │
│ Mudou? SIM → RE-RENDER ✓ │
│ │
│ TodosList: selector = s => s.todos │
│ Antes: [] → Agora: [] │
│ Mudou? NÃO → SKIP RENDER ✓ │
│ │
│ TodosCounter: selector = s => s.todos │
│ Antes: [] → Agora: [] │
│ Mudou? NÃO → SKIP RENDER ✓ │
└─────────────────────────────────────────────────┘
🎯 Resultado: Apenas UserMenu re-renderiza! TodosList e TodosCounter não re-renderizam porque seus seletores retornaram o mesmo valor.
// 🏭 CAMADA 1: Definição genérica
function createStore<TState>(createState) {
// ... implementação Observable
return useStore; // Retorna hook
}
// ⬇️ Uso da factory
// 🗄️ CAMADA 2: Instância específica
export const useGlobalStore = createStore<IGlobalStore>((set, get) => ({
user: null,
login: () => set({ user: {...} })
}));
// ⬇️ Import do hook
// 🎨 CAMADA 3: Consumo nos componentes
function UserMenu() {
const user = useGlobalStore(state => state.user);
// ...
}Arquivo: src/store/createStore.ts
import { useSyncExternalStore } from 'react';
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// TIPOS AUXILIARES
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/**
* SetterFn - Função que recebe estado anterior e retorna parcial
* Exemplo: (prev) => ({ count: prev.count + 1 })
*/
type SetterFn<T> = (prevState: T) => Partial<T>;
/**
* SetStateFn - Aceita objeto parcial OU função setter
* Exemplo: setState({ user: {...} }) OU setState(prev => ({...}))
*/
type SetStateFn<T> = (partialState: Partial<T> | SetterFn<T>) => void;
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// FUNÇÃO PRINCIPAL: createStore
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/**
* Factory function que cria uma store Observable
*
* Esta função é o CORAÇÃO do nosso gerenciador de estado.
* Ela cria uma closure que encapsula:
* - Estado privado
* - Lista de listeners
* - Funções de gerenciamento
*
* @param createState - Função que inicializa o estado
* Recebe setState e getState como parâmetros
* @returns Hook customizado para consumir a store
*
* @example
* const useCounter = createStore<{ count: number }>((set) => ({
* count: 0,
* increment: () => set((prev) => ({ count: prev.count + 1 }))
* }));
*/
export function createStore<TState extends Record<string, any>>(
createState: (setState: SetStateFn<TState>, getState: () => TState) => TState,
) {
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 1️⃣ ESTADO INTERNO (Closure Privada)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/**
* Variável privada que armazena o estado atual
* Só pode ser acessada através de getState()
* Só pode ser modificada através de setState()
*/
let state: TState;
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 2️⃣ LISTA DE OBSERVADORES (Set para evitar duplicatas)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/**
* Set de funções listener (callbacks dos componentes)
* Set é usado ao invés de Array porque:
* - Evita duplicatas automaticamente
* - Operações add/delete são O(1)
*/
let listeners: Set<() => void>;
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 3️⃣ NOTIFICADOR (Executa todos os listeners)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/**
* Notifica todos os componentes inscritos que o estado mudou
* Cada listener causa a execução do selector no useSyncExternalStore
*
* 💡 IMPORTANTE: Não re-renderiza diretamente! Apenas avisa que
* algo mudou. O useSyncExternalStore decide se precisa re-renderizar
* comparando o resultado do selector.
*/
function notifyListeners() {
listeners.forEach((listener) => listener());
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 4️⃣ ATUALIZADOR DE ESTADO (Core do Observable)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/**
* Atualiza o estado e notifica todos os observers
*
* Suporta dois formatos:
* 1. Objeto: setState({ user: {...} })
* 2. Função: setState((prev) => ({ count: prev.count + 1 }))
*
* @param partialState - Objeto parcial ou função que retorna parcial
*
* @example
* // Formato 1: Objeto
* setState({ loading: true })
*
* @example
* // Formato 2: Função (acesso ao estado anterior)
* setState((prev) => ({ count: prev.count + 1 }))
*/
function setState(partialState: Partial<TState> | SetterFn<TState>) {
// Determina o novo valor baseado no tipo de input
const newValue =
typeof partialState === 'function'
? partialState(state) // ← Função: executa com estado atual
: partialState; // ← Objeto: usa diretamente
// Cria novo objeto de estado (IMUTÁVEL)
// Usa spread operator para fazer shallow merge
state = {
...state, // ← Mantém propriedades existentes
...newValue, // ← Sobrescreve apenas as que mudaram
};
// 🔔 CRUCIAL: Notifica todos os componentes inscritos
// Sem isso, a mudança não seria propagada!
notifyListeners();
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 5️⃣ SUBSCRIPTION (Permite componentes observarem)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/**
* Registra um listener (observer) na store
*
* Chamado automaticamente pelo useSyncExternalStore quando
* um componente monta. Retorna função de cleanup que é
* executada quando o componente desmonta.
*
* @param listener - Função callback a ser executada quando estado muda
* @returns Função de unsubscribe (cleanup)
*
* @example
* // Uso direto (raro - normalmente via useSyncExternalStore)
* const unsubscribe = subscribe(() => {
* console.log('Estado mudou!');
* });
*
* // Quando não precisar mais:
* unsubscribe();
*/
function subscribe(listener: () => void) {
listeners.add(listener); // Adiciona à Set de observadores
// Retorna função de cleanup (padrão do React)
// Executada quando componente desmonta
return () => {
listeners.delete(listener); // Remove da Set
};
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 6️⃣ GETTER (Acesso direto ao estado)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/**
* Retorna o estado atual SEM se inscrever para mudanças
*
* Útil quando você precisa do valor dentro de uma ação,
* mas não quer que o componente re-renderize.
*
* @returns O estado completo atual
*
* @example
* addTodo: (title) => {
* const currentUser = getState().user; // Pega user sem inscrever
* setState({ todos: [..., { author: currentUser.name }] });
* }
*/
function getState() {
return state;
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 7️⃣ HOOK DO REACT (Integração com componentes)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/**
* Hook customizado que permite componentes React consumirem a store
*
* Usa useSyncExternalStore (React 18+) para sincronizar
* componente com store externa de forma segura.
*
* O SELECTOR é a chave da otimização:
* - Extrai apenas a parte do estado que o componente precisa
* - useSyncExternalStore compara o resultado e só re-renderiza se mudou
*
* @param selector - Função que extrai valor do estado
* @returns Valor selecionado do estado
*
* @example
* // Seleciona apenas user
* const user = useStore(state => state.user);
*
* @example
* // Seleciona valor derivado
* const todoCount = useStore(state => state.todos.length);
*
* 💡 OTIMIZAÇÃO: Quanto mais granular o selector, melhor!
* ✅ BOM: state => state.user.name
* ⚠️ RUIM: state => state (sempre re-renderiza)
*/
function useStore<TValue>(
selector: (currentState: TState) => TValue,
): TValue {
/**
* useSyncExternalStore é um hook do React 18+ que:
*
* 1. Chama subscribe() uma vez quando componente monta
* - Registra o componente como observer
* - Guarda a função de unsubscribe para cleanup
*
* 2. Chama getSnapshot (selector) inicialmente e após cada notificação
* - Extrai o valor que o componente precisa
*
* 3. Compara o resultado do selector (usando Object.is)
* - Se mudou: agenda re-render
* - Se igual: skip render (OTIMIZAÇÃO!)
*
* Params:
* - subscribe: função que adiciona/remove listener
* - getSnapshot: função que retorna valor atual (selector)
*/
return useSyncExternalStore(subscribe, () => selector(state));
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 8️⃣ INICIALIZAÇÃO
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/**
* Inicializa o estado chamando a função createState
* Passa setState e getState para que ações possam modificar estado
*/
state = createState(setState, getState);
/**
* Inicializa Set vazio de listeners
* Componentes serão adicionados quando montarem
*/
listeners = new Set();
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 9️⃣ RETORNO
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/**
* Retorna apenas o hook useStore
* Estado e listeners ficam encapsulados (closure)
* Padrão de design: Encapsulation + Factory
*/
return useStore;
}Arquivo: src/store/globalStore.ts
import { ITodo } from '../entities/ITodo';
import { IUser } from '../entities/IUser';
import { createStore } from './createStore';
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// INTERFACE DA STORE (Contrato TypeScript)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/**
* Interface que define o formato completo da store global
*
* Combina:
* - Estado (user, todos)
* - Ações (login, logout, addTodo, etc)
*
* TypeScript garante type-safety em toda aplicação
*/
interface IGlobalStore {
// ─────────────────────────────────────────
// ESTADO
// ─────────────────────────────────────────
user: IUser | null; // null = não autenticado
todos: ITodo[]; // Array de tarefas
// ─────────────────────────────────────────
// AÇÕES DE AUTENTICAÇÃO
// ─────────────────────────────────────────
login(): void;
logout(): void;
// ─────────────────────────────────────────
// AÇÕES DE TODOS
// ─────────────────────────────────────────
addTodo(title: string): void;
toggleTodoDone(todoId: number): void;
removeTodo(todoId: number): void;
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// CRIAÇÃO DA STORE GLOBAL
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/**
* Store global da aplicação
*
* Criada usando a factory createStore<T>
* Exporta um hook: useGlobalStore
*
* Este hook pode ser usado em qualquer componente sem Provider!
*/
export const useGlobaStore = createStore<IGlobalStore>(
(setState, getState) => ({
// ─────────────────────────────────────────
// 📊 ESTADO INICIAL
// ─────────────────────────────────────────
user: null, // User não autenticado
todos: [], // Array vazio de todos
// ─────────────────────────────────────────
// 🔐 AÇÕES DE AUTENTICAÇÃO
// ─────────────────────────────────────────
/**
* Faz login do usuário
*
* Em app real:
* - Faria chamada à API
* - Validaria credenciais
* - Salvaria token
*
* Aqui: Seta user hardcoded para fins educacionais
*/
login: () =>
setState({
user: {
email: 'mbreno.dev@gmail.com',
name: 'MB',
},
}),
/**
* Faz logout do usuário
*
* Simplesmente seta user como null
* Todos permanecem (não são limpos)
*/
logout: () => setState({ user: null }),
// ─────────────────────────────────────────
// ✅ AÇÕES DE TODOS
// ─────────────────────────────────────────
/**
* Adiciona nova tarefa
*
* 💡 IMPORTANTE: Usa setState com FUNÇÃO para acessar prevState
* 💡 IMPORTANTE: Usa getState() para pegar user atual
*
* Padrão de imutabilidade:
* - Usa .concat() ao invés de .push()
* - .concat() retorna NOVO array
* - Estado anterior permanece intacto
*
* @param title - Título da tarefa
*/
addTodo: (title: string) => {
setState((prevState) => ({
todos: prevState.todos.concat({
id: Date.now(), // ID simples (em produção: UUID)
title,
author: getState().user?.name ?? 'Convidado', // ← Usa getState()!
done: false,
}),
}));
},
/**
* Alterna status done da tarefa
*
* Padrão de imutabilidade:
* - Usa .map() para criar NOVO array
* - Para o todo matching: cria novo objeto com spread
* - Para outros todos: mantém referência (otimização)
*
* ⚡ OTIMIZAÇÃO: Apenas o todo alterado é um novo objeto
* Os demais mantêm a mesma referência, ajudando React.memo
*
* @param todoId - ID da tarefa a alterar
*/
toggleTodoDone: (todoId: number) => {
setState((prevState) => ({
todos: prevState.todos.map((todo) =>
todo.id === todoId
? { ...todo, done: !todo.done } // ← Cria novo objeto
: todo // ← Mantém referência
),
}));
},
/**
* Remove tarefa
*
* Padrão de imutabilidade:
* - Usa .filter() para criar NOVO array
* - Retorna apenas os todos que NÃO tem o ID
*
* @param todoId - ID da tarefa a remover
*/
removeTodo: (todoId: number) => {
setState((prevState) => ({
todos: prevState.todos.filter((todo) => todo.id !== todoId),
}));
},
}),
);Arquivo: src/components/UserMenu.tsx
import { useGlobaStore } from '../store/globalStore';
export function UserMenu() {
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// SELETORES GRANULARES
// Cada chamada cria uma subscription independente
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/**
* Seleciona apenas user
*
* ✅ RE-RENDERIZA quando: user muda (login/logout)
* ✅ NÃO re-renderiza quando: todos mudam
*/
const user = useGlobaStore((state) => state.user);
/**
* Seleciona ação login
*
* ✅ NUNCA re-renderiza (ações têm referência estável)
*
* Por que? A função login é criada uma vez no início
* e sempre retorna a mesma referência. Object.is(prevLogin, login) = true
*/
const login = useGlobaStore((state) => state.login);
/**
* Seleciona ação logout
* Mesma lógica da login
*/
const logout = useGlobaStore((state) => state.logout);
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// RENDERIZAÇÃO CONDICIONAL
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
if (!user) {
return (
<button onClick={login}>
Entrar
</button>
);
}
return (
<div>
<span>Olá, {user.name}!</span>
<button onClick={logout}>Sair</button>
</div>
);
}🔍 Análise de Re-renders:
CENÁRIO 1: User faz login
setState({ user: {...} })
↓
UserMenu selector: state => state.user
Antes: null
Agora: { name: 'MB', email: '...' }
Mudou? SIM
✅ RE-RENDERIZA
CENÁRIO 2: User adiciona todo
setState({ todos: [...novo todo] })
↓
UserMenu selector: state => state.user
Antes: { name: 'MB', ... }
Agora: { name: 'MB', ... } (mesma referência!)
Mudou? NÃO
✅ SKIP - NÃO RE-RENDERIZA
Arquivo: src/components/TodosCounter.tsx
import { useGlobaStore } from '../store/globalStore';
export function TodosCounter() {
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// SELECTOR COM DADO DERIVADO
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/**
* Seleciona ARRAY completo de todos
*
* ⚠️ ATENÇÃO: Re-renderiza toda vez que todos[] muda
* Mesmo que só o título de um todo tenha mudado!
*
* Por que? Porque o array é uma nova referência:
* prevTodos !== newTodos (sempre)
*/
const todos = useGlobaStore((state) => state.todos);
/**
* Calcula length localmente
*
* ⚠️ PROBLEMA: Cálculo roda toda vez, mas re-render já aconteceu
*/
const totalTodos = todos.length;
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 💡 OTIMIZAÇÃO POSSÍVEL
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
/**
* Alternativa mais otimizada:
*
* const totalTodos = useGlobaStore(state => state.todos.length);
*
* Por que é melhor?
* - Selector retorna NUMBER (primitivo)
* - Compara valor: 3 === 3 (pode ser true!)
* - Só re-renderiza quando LENGTH realmente muda
*
* Exemplo:
* toggleTodoDone(1) altera array mas length permanece 3
* Antes: 3
* Agora: 3
* Mudou? NÃO
* ✅ SKIP RENDER!
*/
return (
<div>
{totalTodos === 0 ? (
<span>Nenhuma tarefa!</span>
) : (
<span>Tarefas: {totalTodos}</span>
)}
</div>
);
}🎯 Comparação de Otimizações:
// ❌ MENOS OTIMIZADO (re-renderiza mais)
const todos = useGlobaStore(state => state.todos);
const count = todos.length;
// Re-renderiza: addTodo ✓, toggleDone ✓, removeTodo ✓
// ✅ MAIS OTIMIZADO (re-renderiza menos)
const count = useGlobaStore(state => state.todos.length);
// Re-renderiza: addTodo ✓, toggleDone ✗, removeTodo ✓Arquivo: src/components/TodoForm.tsx
import { useRef, memo } from 'react';
import { useGlobaStore } from '../store/globalStore';
/**
* Componente interno (não exportado)
* Será envolvido com memo()
*/
function TodoFormComponent() {
const inputRef = useRef<HTMLInputElement | null>(null);
/**
* Seleciona APENAS a ação addTodo
*
* ✅ Como ações têm referência estável, este selector
* SEMPRE retorna a mesma função
* ✅ useSyncExternalStore nunca agenda re-render
* ✅ Combinado com memo(), componente NUNCA re-renderiza!
*/
const addTodo = useGlobaStore((state) => state.addTodo);
function handleSubmit(event: React.FormEvent) {
event.preventDefault();
if (inputRef.current?.value) {
addTodo(inputRef.current.value);
inputRef.current.value = ''; // Limpa input
}
}
return (
<form onSubmit={handleSubmit}>
<input
ref={inputRef}
placeholder="Título da tarefa..."
/>
<button type="submit">Enviar</button>
</form>
);
}
/**
* Exporta versão memoizada
*
* memo() evita re-renders quando props não mudam
* Como não tem props e selector retorna valor estável,
* este componente renderiza apenas 1x (montagem)!
*
* ⚡ PERFORMANCE: Combinação perfeita!
* - Selector granular (só ação)
* - memo() (sem props)
* - Resultado: 0 re-renders após montagem
*/
export const TodoForm = memo(TodoFormComponent);Crie sua própria store usando a factory createStore:
// myStore.ts
import { createStore } from './createStore';
// 1️⃣ Defina a interface (contrato TypeScript)
interface IMyStore {
count: number;
increment(): void;
decrement(): void;
reset(): void;
}
// 2️⃣ Crie a store
export const useMyStore = createStore<IMyStore>((setState, getState) => ({
// Estado inicial
count: 0,
// Ações
increment: () => setState((prev) => ({ count: prev.count + 1 })),
decrement: () => setState((prev) => ({ count: prev.count - 1 })),
reset: () => setState({ count: 0 }),
}));Use seletores para extrair apenas o que precisa:
// Counter.tsx
import { useMyStore } from './myStore';
function Counter() {
// Seletores granulares
const count = useMyStore((state) => state.count);
const increment = useMyStore((state) => state.increment);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
</div>
);
}Cada componente se inscreve apenas no que usa:
// Display.tsx - Só exibe
function Display() {
// ✅ Só re-renderiza quando count muda
const count = useMyStore((state) => state.count);
return <h1>{count}</h1>;
}
// Controls.tsx - Só ações
function Controls() {
// ✅ NUNCA re-renderiza (ações são estáveis)
const increment = useMyStore((state) => state.increment);
const decrement = useMyStore((state) => state.decrement);
const reset = useMyStore((state) => state.reset);
return (
<div>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
}
// App.tsx - Combina tudo
function App() {
return (
<>
<Display /> {/* Re-renderiza quando count muda */}
<Controls /> {/* NUNCA re-renderiza */}
</>
);
}// Extrai apenas o necessário
const userName = useGlobalStore((state) => state.user?.name);
const userEmail = useGlobalStore((state) => state.user?.email);Por que? Componente só re-renderiza quando o valor específico muda.
// EVITE: Seleciona tudo
const state = useGlobalStore((state) => state);
// Problema: Re-renderiza para QUALQUER mudança no estadoPor que evitar? Perde toda a otimização de seletores granulares.
// Cada chamada é um selector independente
const user = useGlobalStore((state) => state.user);
const todos = useGlobalStore((state) => state.todos);
const addTodo = useGlobalStore((state) => state.addTodo);Por que? Cada selector tem controle fino sobre quando re-renderizar.
// EVITE: Cria objeto novo toda vez
const { user, todos } = useGlobalStore((state) => ({
user: state.user,
todos: state.todos,
}));
// Problema: { user, todos } é SEMPRE um novo objeto
// prevObj !== newObj (sempre true!)
// Resultado: Re-renderiza SEMPREPor que evitar? O selector retorna novo objeto a cada execução, causando re-renders desnecessários.
// Deriva dados sem armazenar
const completedTodos = useGlobalStore((state) =>
state.todos.filter((todo) => todo.done)
);
const completedCount = completedTodos.length;Vantagem: Dados sempre sincronizados, sem estado duplicado.
// ⚠️ Re-renderiza toda vez (novo array)
const completedTodos = useGlobalStore((state) =>
state.todos.filter((todo) => todo.done)
);
// 💡 Para otimizar, use biblioteca de memoization como:
// - reselect
// - zustand/middleware (shallow)function TodoItem({ id }: { id: number }) {
// Busca todo específico por ID
const todo = useGlobalStore((state) =>
state.todos.find((t) => t.id === id)
);
if (!todo) return null;
return <div>{todo.title}</div>;
}export const useMyStore = createStore<IMyStore>((setState) => ({
data: null,
loading: false,
error: null,
fetchData: async () => {
// 1️⃣ Inicia loading
setState({ loading: true, error: null });
try {
// 2️⃣ Faz requisição
const response = await fetch('/api/data');
const data = await response.json();
// 3️⃣ Sucesso: atualiza data
setState({ data, loading: false });
} catch (error) {
// 4️⃣ Erro: atualiza error
setState({ error: error.message, loading: false });
}
},
}));function ExpensiveComponent() {
const todos = useGlobalStore((state) => state.todos);
// Memoiza cálculo pesado
const stats = useMemo(() => {
return {
total: todos.length,
completed: todos.filter((t) => t.done).length,
pending: todos.filter((t) => !t.done).length,
};
}, [todos]); // Só recalcula quando todos muda
return <div>Total: {stats.total}</div>;
}| Característica | Context API | Observable Store |
|---|---|---|
| Provider necessário | ✅ Sim (obrigatório) | ❌ Não (uso direto) |
| Re-renders desnecessários | ✅ Não (com seletores) | |
| Granularidade | ❌ Todos os consumidores | ✅ Por selector |
| Boilerplate | ✅ Baixo (createStore) | |
| Type-safety | ✅ TypeScript nativo | |
| DevTools | ✅ React DevTools built-in | ❌ Não nativo (pode adicionar) |
| Curva de aprendizado | ✅ Menor (API do React) | |
| Performance (apps grandes) | ❌ Pior (muitos re-renders) | ✅ Melhor (otimizado) |
| Código duplicado | ✅ Apenas hook | |
| Subscription explícita | ❌ Implícita (useContext) | ✅ Explícita (selector) |
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 1️⃣ Criar Context
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
const AppContext = createContext(null);
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 2️⃣ Criar Provider Component
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [todos, setTodos] = useState([]);
const login = () => setUser({ name: 'MB', email: 'mb@dev.com' });
const logout = () => setUser(null);
const addTodo = (title) => setTodos([...todos, { id: Date.now(), title }]);
// ⚠️ PROBLEMA: Qualquer mudança aqui re-renderiza TODOS os consumidores
return (
<AppContext.Provider value={{ user, todos, login, logout, addTodo }}>
{children}
</AppContext.Provider>
);
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 3️⃣ Envolver App com Provider
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
function Root() {
return (
<AppProvider>
<App />
</AppProvider>
);
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 4️⃣ Consumir
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
function UserMenu() {
const { user, login, logout } = useContext(AppContext);
// ⚠️ PROBLEMA: Re-renderiza quando 'todos' muda, mesmo não usando!
// Porque o objeto { user, todos, login... } é novo toda vez
if (!user) return <button onClick={login}>Entrar</button>;
return <button onClick={logout}>Sair ({user.name})</button>;
}📊 Resultado com Context API:
- Adicionar todo → UserMenu re-renderiza ❌
- Fazer login → UserMenu re-renderiza ✅
- Fazer logout → UserMenu re-renderiza ✅
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 1️⃣ Criar Store (arquivo único, sem Provider!)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
export const useGlobalStore = createStore<IGlobalStore>((setState) => ({
user: null,
todos: [],
login: () => setState({ user: { name: 'MB', email: 'mb@dev.com' } }),
logout: () => setState({ user: null }),
addTodo: (title) =>
setState((prev) => ({
todos: [...prev.todos, { id: Date.now(), title }],
})),
}));
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 2️⃣ Usar DIRETAMENTE (sem Provider!)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
function UserMenu() {
// Seletores granulares
const user = useGlobalStore((state) => state.user);
const login = useGlobalStore((state) => state.login);
const logout = useGlobalStore((state) => state.logout);
// ✅ SOLUÇÃO: Só re-renderiza quando 'user' muda!
if (!user) return <button onClick={login}>Entrar</button>;
return <button onClick={logout}>Sair ({user.name})</button>;
}📊 Resultado com Observable Store:
- Adicionar todo → UserMenu NÃO re-renderiza ✅
- Fazer login → UserMenu re-renderiza ✅
- Fazer logout → UserMenu re-renderiza ✅
// Total: ~50 linhas de código
- createContext (1 linha)
- Provider component (~20 linhas)
- useState para cada state (2 linhas)
- Wrapper em Root (3 linhas)
- useContext em cada component (1 linha cada)
- Hooks customizados (opcional) (~10 linhas)// Total: ~30 linhas de código
- createStore call (~15 linhas para estado + ações)
- Import e uso (1 linha)
- Sem Provider (0 linhas)
- Sem hooks wrapper (0 linhas)💡 Redução: ~40% menos código
- Apps muito pequenos (1-3 telas)
- Dados que mudam raramente (tema, idioma, feature flags)
- Quando não há problemas de performance
- Time familiarizado apenas com React APIs
- Necessidade de DevTools nativo
- Apps médios a grandes (5+ telas)
- Estado que muda frequentemente (dados de usuário, listas, formulários)
- Performance é prioridade (muitos componentes, listas longas)
- Necessidade de granularidade (componentes devem re-renderizar seletivamente)
- Estado complexo com muitas ações e transformações
- Preferência por code splitting (store fora da árvore React)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// ANTES: Context API
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 1. Definição
const UserContext = createContext(null);
// 2. Provider
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const login = () => setUser({...});
const logout = () => setUser(null);
return (
<UserContext.Provider value={{ user, login, logout }}>
{children}
</UserContext.Provider>
);
}
// 3. Hook customizado (opcional)
function useUser() {
const context = useContext(UserContext);
if (!context) throw new Error('useUser must be within UserProvider');
return context;
}
// 4. Uso
function Component() {
const { user, login, logout } = useUser();
// ...
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// DEPOIS: Observable Store
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 1. Definição + Store (tudo em um)
export const useUserStore = createStore((setState) => ({
user: null,
login: () => setState({ user: {...} }),
logout: () => setState({ user: null }),
}));
// 2. Uso direto (sem Provider!)
function Component() {
const user = useUserStore((state) => state.user);
const login = useUserStore((state) => state.login);
const logout = useUserStore((state) => state.logout);
// ...
}🎯 Mudanças necessárias:
- Remover
createContext - Remover componente
Provider - Remover wrapper
<Provider>do App - Trocar
useContextpor seletoresuseStore(selector) - Adicionar
createStore.tsao projeto
P: O que é o padrão de projeto Observable? R: É um padrão de design onde um objeto (Subject) mantém uma lista de observers (inscritos) e os notifica automaticamente sobre mudanças em seu estado. É como um sistema de notificações push.
P: Qual é a função do método notifyListeners?
R: Executar todos os listeners inscritos, informando-os de que o estado mudou. Ele percorre a Set de listeners e executa cada função callback.
P: Por que usar Set ao invés de Array para armazenar listeners?
R: Set evita automaticamente a duplicação de elementos, garantindo que cada listener seja único. Além disso, operações de add e delete são O(1), mais rápidas que Array.
P: Como desinscrever um listener de um Set?
R: Usando o método delete() com a referência do listener: listeners.delete(listener). A função subscribe retorna essa cleanup function automaticamente.
P: Qual é a diferença entre Array e Set em JavaScript? R: Array permite elementos duplicados, enquanto Set não permite. Set também oferece operações mais eficientes para adicionar/remover elementos.
P: Qual é o principal problema ao usar Context API? R: Todos os componentes que acessam o contexto são re-renderizados quando qualquer parte do valor do contexto muda, mesmo que não usem os dados atualizados. Falta granularidade.
P: Qual é a função do Provider no Context API?
R: Fornecer um valor global que pode ser acessado por componentes filhos através do useContext. Ele cria um "escopo" onde o valor está disponível.
P: O Zustand (ou este gerenciador) requer o uso de um Provider para funcionar? R: Falso. Zustand e este gerenciador não precisam de Provider. A store existe fora da árvore React e pode ser acessada diretamente via hook.
P: Qual é a vantagem de usar gerenciadores de estado externos? R: Evitam renderizações desnecessárias de componentes através do uso de seletores granulares. Cada componente só re-renderiza quando a parte específica do estado que ele usa realmente muda.
P: O que é o hook useSyncExternalStore?
R: É um hook do React 18+ que permite sincronizar um componente com uma store externa (fora do estado do React) de forma segura, evitando inconsistências durante renderização concorrente.
P: Como funciona a integração do Observable com React?
R: Através do useSyncExternalStore, que recebe uma função subscribe (para registrar listeners) e uma função getSnapshot (selector que extrai valor atual). O React gerencia a inscrição e compara valores para decidir re-renders.
P: Por que as ações (como login, logout) não causam re-render?
R: Porque são funções com referência estável (criadas uma vez no início e nunca recriadas). O selector sempre retorna a mesma função, então Object.is(prevLogin, login) === true.
P: Como evitar re-renders desnecessários?
R: Usando seletores granulares que extraem apenas a parte específica do estado que o componente realmente usa. Exemplo: state => state.user.name ao invés de state => state.
P: O que acontece quando setState é chamado?
R: 1) O estado interno é atualizado com merge imutável, 2) notifyListeners é chamado, 3) Todos os listeners executam, 4) useSyncExternalStore executa seletores, 5) Compara resultados e agenda re-renders apenas se mudou.
P: É possível usar múltiplas stores?
R: Sim! Você pode criar quantas stores quiser usando createStore. Cada uma é independente com seu próprio estado e listeners. Exemplo: useAuthStore, useTodosStore, useUIStore.
P: Quando devo usar getState() ao invés de setState?
R: Use getState() dentro de ações quando precisar ler o valor atual do estado SEM se inscrever para mudanças. Útil para lógica que depende de múltiplas partes do estado.
- Node.js 18 ou superior
- npm ou yarn (gerenciador de pacotes)
# Clone o repositório
git clone <url-do-repositorio>
# Entre na pasta do projeto
cd zustand
# Instale as dependências
yarn install
# ou
npm install# Modo desenvolvimento (com hot reload)
yarn dev
# ou
npm run dev
# Acessar no navegador:
# http://localhost:5173# Build para produção
yarn build
# ou
npm run build
# Preview do build de produção
yarn preview
# ou
npm run preview- Abra o DevTools no navegador (F12)
- Vá para a aba Console
- Interaja com a aplicação:
🧪 EXPERIMENTO 1: Fazer Login
1. Clique em "Entrar"
2. Observe o console:
✅ UserMenu renderizou 2 vezes
✅ TodosList NÃO re-renderizou
✅ TodosCounter NÃO re-renderizou
Por quê? Apenas UserMenu usa state.user!
🧪 EXPERIMENTO 2: Adicionar Todo
1. Digite um título e clique "Enviar"
2. Observe o console:
✅ UserMenu NÃO re-renderizou
✅ TodosList renderizou (usa state.todos)
✅ TodosCounter renderizou (usa state.todos)
✅ TodoForm NÃO re-renderizou (memo + ação estável)
Por quê? Apenas componentes que usam todos re-renderizam!
🧪 EXPERIMENTO 3: Toggle Todo
1. Clique no círculo para marcar como feito
2. Observe o console:
✅ UserMenu NÃO re-renderizou
✅ TodosList renderizou (array mudou)
✅ TodosCounter renderizou (array mudou, mesmo length igual)
Oportunidade de otimização: TodosCounter poderia
selecionar apenas .length ao invés do array completo!
O projeto inclui o hook useRenderCounter em todos os componentes:
// src/hooks/useRenderCounter.ts
export function useRenderCounter(componentName: string) {
const counter = useRef(0);
counter.current += 1;
console.log(`${componentName} renderizou ${counter.current} vezes.`);
}Como usar em seus componentes:
import { useRenderCounter } from '../hooks/useRenderCounter';
function MyComponent() {
useRenderCounter('MyComponent');
// ... resto do componente
}zustand/
├── src/
│ ├── store/
│ │ ├── createStore.ts ← Observable implementation
│ │ └── globalStore.ts ← App store
│ ├── components/
│ │ ├── AppBar.tsx
│ │ ├── UserMenu.tsx
│ │ ├── TodoForm.tsx
│ │ ├── TodosList.tsx
│ │ └── TodosCounter.tsx
│ ├── hooks/
│ │ └── useRenderCounter.ts
│ └── main.tsx
├── package.json
└── vite.config.ts
- React useSyncExternalStore - Documentação oficial do hook
- Zustand - Biblioteca que inspirou este projeto
- Observable Pattern - Explicação do padrão de design
- You Might Not Need Context - Kent C. Dodds sobre gerenciamento de estado
- Context vs External Store - Comparação detalhada
- useSyncExternalStore Deep Dive - Como o hook funciona internamente
- Zustand - Gerenciador minimalista (~1kb)
- Jotai - Atomic state management
- Valtio - Proxy-based state
- Redux Toolkit - Redux moderno e opinado
- Middleware: Adicionar logger, persist (localStorage), devtools
- Computed values: Memoização automática de seletores derivados
- DevTools: Integração com Redux DevTools
- Testes: Testes unitários para store e componentes
- Múltiplas stores: Exemplo com stores separadas (auth, ui, data)
- Immer integration: Mutações "aparentes" com Immer
- Async actions: Middleware para gerenciar loading/error states
- Projeto desenvolvido como material de estudo baseado na Aula #002 sobre gerenciamento de estado em React
- Inspirado no Zustand por Poimandres
- Implementação educacional para demonstrar padrão Observable
Este projeto é open source e está disponível sob a Licença MIT.
Contribuições são bem-vindas! Sinta-se à vontade para:
- Abrir issues com dúvidas ou sugestões
- Enviar pull requests com melhorias
- Compartilhar este repositório como material de estudo
Feito com 💙 para aprender e ensinar gerenciamento de estado em React