Skip to content

mbdevlabs/jstack-zustand

Repository files navigation

Gerenciador de Estado Customizado com Observable Pattern

Construindo um Zustand do Zero - Guia Completo de Estudos

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.

TypeScript React Vite License


📚 Índice

  1. Sobre o Projeto
  2. O Problema com Context API
  3. Introdução ao Padrão Observable
  4. Arquitetura da Solução
  5. Implementação Detalhada
  6. Guia de Uso
  7. Comparação: Context API vs Este Gerenciador
  8. Perguntas e Respostas
  9. Como Executar
  10. Recursos Adicionais

🎯 Sobre o Projeto

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

🎓 Objetivos de Aprendizado

Ao estudar este projeto, você vai entender:

  1. Os problemas do Context API em aplicações grandes
  2. Como implementar um gerenciador de estado do zero
  3. Por que o padrão Observable evita re-renders desnecessários
  4. Como usar seletores para otimizar performance
  5. Quando escolher entre Context API e gerenciadores externos

🛠️ Tecnologias Utilizadas

  • 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

⚠️ O Problema com Context API

Renderizações em Cascata

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.

Exemplo Problemático

// ❌ 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'
}

Por que isso acontece?

  1. Provider único: Todo o estado fica em um único objeto de contexto
  2. Sem granularidade: React não consegue saber qual parte do contexto cada componente realmente usa
  3. Reconciliação conservadora: Por segurança, React re-renderiza todos os consumidores quando o contexto muda

Visualização do Problema

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)

Consequências Reais

  • 📉 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)

Tentativas de Solução com Context API

// ⚠️ 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!


🔭 Introdução ao Padrão Observable

Conceito Fundamental

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.

Analogia: Canal do YouTube

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

Componentes do Padrão

1. Subject (Observável)

O objeto que mantém o estado e a lista de observers.

let state = { user: null, todos: [] };  // Estado
let listeners = new Set();               // Observadores

2. Observers (Observadores)

Funções/objetos que reagem às mudanças.

function componentListener() {
  console.log('Estado mudou! Preciso re-renderizar');
}

3. Subscribe

Método para registrar um observador.

function subscribe(listener) {
  listeners.add(listener);        // Adiciona à lista
  return () => {                  // Retorna unsubscribe
    listeners.delete(listener);
  };
}

4. Notify

Método que avisa todos os observadores.

function notifyListeners() {
  listeners.forEach(listener => listener());
}

Por que usar Set ao invés de Array?

// ❌ 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 1x

Vantagens do Set:

  • ✅ Evita duplicação automática
  • ✅ Operações add e delete são O(1) - muito rápidas
  • ✅ Não precisa verificar se já existe antes de adicionar
  • ✅ Código mais limpo e seguro

Fluxo Completo do Observable Pattern

┌──────────────────────────────────────────────────┐
│ 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 ✓              │
└──────────────────────────────────────────────────┘

Exemplo Prático

// 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!"

🏗️ Arquitetura da Solução

Visão Geral em Camadas

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               │
└────────────────────────────────────────────────────┘

Estrutura de Arquivos

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

Fluxo de Dados Completo

┌─────────────────────────────────────────────────┐
│ 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.

Comunicação Entre Camadas

// 🏭 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);
  // ...
}

💻 Implementação Detalhada

7.1 createStore - A Factory Observable

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;
}

7.2 globalStore - Instância da Aplicação

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),
      }));
    },
  }),
);

7.3 Consumo nos Componentes

Exemplo 1: UserMenu - Seletores Simples

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

Exemplo 2: TodosCounter - Selector Derivado

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 ✓

Exemplo 3: TodoForm - Componente Memoizado

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);

📖 Guia de Uso

Passo 1: Criar uma Store

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 }),
}));

Passo 2: Consumir nos Componentes

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>
  );
}

Passo 3: Múltiplos Componentes

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 */}
    </>
  );
}

Boas Práticas

✅ DO: Seletores Granulares

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

❌ DON'T: Selecionar Estado Inteiro

// EVITE: Seleciona tudo
const state = useGlobalStore((state) => state);

// Problema: Re-renderiza para QUALQUER mudança no estado

Por que evitar? Perde toda a otimização de seletores granulares.

✅ DO: Múltiplas Chamadas ao Hook

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

❌ DON'T: Desestruturar Objeto

// 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 SEMPRE

Por que evitar? O selector retorna novo objeto a cada execução, causando re-renders desnecessários.

✅ DO: Seletores com Computação

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

⚠️ ATENÇÃO: Seletores com Objetos/Arrays

// ⚠️ 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)

Padrões Avançados

Selector com Parâmetro

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>;
}

Ações Assíncronas

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 });
    }
  },
}));

Selector Combinado com useMemo

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>;
}

⚖️ Comparação: Context API vs Este Gerenciador

Tabela Comparativa

Característica Context API Observable Store
Provider necessário ✅ Sim (obrigatório) ❌ Não (uso direto)
Re-renders desnecessários ⚠️ Sim (problema crítico) ✅ Não (com seletores)
Granularidade ❌ Todos os consumidores ✅ Por selector
Boilerplate ⚠️ Médio (Provider, Context, hooks) ✅ Baixo (createStore)
Type-safety ⚠️ Requer cuidado manual ✅ TypeScript nativo
DevTools ✅ React DevTools built-in ❌ Não nativo (pode adicionar)
Curva de aprendizado ✅ Menor (API do React) ⚠️ Média (novos conceitos)
Performance (apps grandes) ❌ Pior (muitos re-renders) ✅ Melhor (otimizado)
Código duplicado ⚠️ Hooks wrapper + Provider ✅ Apenas hook
Subscription explícita ❌ Implícita (useContext) ✅ Explícita (selector)

Exemplo Lado a Lado

Com Context API

// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 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 ✅

Com Este Gerenciador (Observable)

// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 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 ✅

Linha de Código e Complexidade

Context API

// 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)

Observable Store

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

Casos de Uso

✅ Quando usar Context API

  1. Apps muito pequenos (1-3 telas)
  2. Dados que mudam raramente (tema, idioma, feature flags)
  3. Quando não há problemas de performance
  4. Time familiarizado apenas com React APIs
  5. Necessidade de DevTools nativo

✅ Quando usar Observable Store (como este)

  1. Apps médios a grandes (5+ telas)
  2. Estado que muda frequentemente (dados de usuário, listas, formulários)
  3. Performance é prioridade (muitos componentes, listas longas)
  4. Necessidade de granularidade (componentes devem re-renderizar seletivamente)
  5. Estado complexo com muitas ações e transformações
  6. Preferência por code splitting (store fora da árvore React)

Migração de Context para Observable Store

// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 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:

  1. Remover createContext
  2. Remover componente Provider
  3. Remover wrapper <Provider> do App
  4. Trocar useContext por seletores useStore(selector)
  5. Adicionar createStore.ts ao projeto

❓ Perguntas e Respostas

Fundamentos do Observable Pattern

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.

Context API vs Gerenciadores de Estado

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.

Implementação e React

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.


🚀 Como Executar o Projeto

Pré-requisitos

  • Node.js 18 ou superior
  • npm ou yarn (gerenciador de pacotes)

Instalação

# 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

Execução

# 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

Explorando o Código e Observando Re-renders

  1. Abra o DevTools no navegador (F12)
  2. Vá para a aba Console
  3. 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!

Debug de Re-renders

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
}

Estrutura do Projeto

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

📚 Recursos Adicionais

Documentação Oficial

Artigos Recomendados

Bibliotecas Similares

Próximos Passos para Expandir

  • 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

Créditos

  • 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

📝 Licença

Este projeto é open source e está disponível sob a Licença MIT.


🤝 Contribuindo

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

About

Zustand recriado do zero — implementacao custom de state management com Observable pattern

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors