Gerenciamento de estado com Zustand

1. Introdução ao Zustand e seus Fundamentos

Zustand é um gerenciador de estado leve e minimalista para React, criado por Paul Henschel. Sua principal proposta é oferecer uma API simples e direta para gerenciar estado global sem o boilerplate excessivo de soluções como Redux ou a complexidade de Context API para aplicações de médio porte.

Comparado ao Redux, Zustand elimina a necessidade de criar actions, reducers e dispatchers separados. Enquanto a Context API pode causar re-renderizações desnecessárias em componentes que não consomem o estado alterado, Zustand oferece seletores precisos que evitam esse problema. A instalação é trivial:

npm install zustand

2. Criando e Utilizando Stores

A estrutura de uma store em Zustand é criada com a função create(), que recebe um callback contendo estado, ações e getters:

import { create } from 'zustand';

const useCounterStore = create((set, get) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
  getDoubleCount: () => get().count * 2,
}));

Para acessar o estado em componentes, utilizamos o hook gerado automaticamente:

function Counter() {
  const count = useCounterStore((state) => state.count);
  const increment = useCounterStore((state) => state.increment);

  return (
    <div>
      <p>Contagem: {count}</p>
      <button onClick={increment}>Incrementar</button>
    </div>
  );
}

A função set permite atualizações imutáveis de forma intuitiva, enquanto get fornece acesso ao estado atual dentro de ações.

3. Ações e Atualizações Complexas

Ações síncronas são definidas diretamente na store. Para operações assíncronas, como chamadas a APIs, as ações podem usar async/await:

const useUserStore = create((set, get) => ({
  user: null,
  loading: false,
  error: null,

  fetchUser: async (userId) => {
    set({ loading: true, error: null });
    try {
      const response = await fetch(`https://api.example.com/users/${userId}`);
      const data = await response.json();
      set({ user: data, loading: false });
    } catch (error) {
      set({ error: error.message, loading: false });
    }
  },

  updateUser: async (userData) => {
    const previousUser = get().user;
    set({ user: { ...previousUser, ...userData } });
    try {
      await fetch(`https://api.example.com/users/${userData.id}`, {
        method: 'PUT',
        body: JSON.stringify(userData),
      });
    } catch (error) {
      set({ user: previousUser }); // rollback em caso de erro
    }
  },
}));

O uso de set com funções é recomendado para evitar race conditions quando múltiplas atualizações ocorrem simultaneamente.

4. Seletores e Performance

Para otimizar re-renderizações, utilizamos seletores que extraem apenas partes específicas do estado:

const userName = useUserStore((state) => state.user?.name);
const userEmail = useUserStore((state) => state.user?.email);

Seletores customizados podem ser criados para evitar recálculos desnecessários:

const useUserFullName = () => {
  return useUserStore((state) => {
    const user = state.user;
    return user ? `${user.firstName} ${user.lastName}` : '';
  });
};

Para comparação rasa de objetos, o utilitário shallow é essencial:

import { shallow } from 'zustand/shallow';

function UserProfile() {
  const [name, email] = useUserStore(
    (state) => [state.user?.name, state.user?.email],
    shallow
  );

  return <div>{name} - {email}</div>;
}

5. Stores Múltiplas e Composição

Separar estado em stores independentes por domínio é uma prática recomendada:

// authStore.js
const useAuthStore = create((set) => ({
  user: null,
  token: null,
  login: (credentials) => {
    // lógica de autenticação
    set({ user: credentials.user, token: credentials.token });
  },
  logout: () => set({ user: null, token: null }),
}));

// cartStore.js
const useCartStore = create((set, get) => ({
  items: [],
  addItem: (product) => {
    const currentItems = get().items;
    const existingItem = currentItems.find(item => item.id === product.id);

    if (existingItem) {
      set({
        items: currentItems.map(item =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        ),
      });
    } else {
      set({ items: [...currentItems, { ...product, quantity: 1 }] });
    }
  },
  getTotal: () => {
    return get().items.reduce((total, item) => total + item.price * item.quantity, 0);
  },
}));

A comunicação entre stores é feita importando e chamando ações diretamente:

const useCheckoutStore = create((set, get) => ({
  processCheckout: async () => {
    const authState = useAuthStore.getState();
    const cartState = useCartStore.getState();

    if (!authState.token) {
      throw new Error('Usuário não autenticado');
    }

    const response = await fetch('/api/checkout', {
      method: 'POST',
      headers: { Authorization: `Bearer ${authState.token}` },
      body: JSON.stringify({ items: cartState.items, total: cartState.getTotal() }),
    });

    if (response.ok) {
      useCartStore.getState().clearCart();
    }
  },
}));

6. Middleware e Persistência

Zustand oferece middleware nativos como persist, devtools e immer:

import { persist, devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';

const useSettingsStore = create(
  devtools(
    persist(
      immer((set) => ({
        theme: 'light',
        language: 'pt-BR',
        notifications: true,
        toggleTheme: () => set((state) => {
          state.theme = state.theme === 'light' ? 'dark' : 'light';
        }),
        setLanguage: (lang) => set((state) => {
          state.language = lang;
        }),
      })),
      {
        name: 'app-settings',
        getStorage: () => localStorage,
      }
    ),
    { name: 'SettingsStore' }
  )
);

O middleware persist salva automaticamente o estado no localStorage. O devtools permite debug com Redux DevTools. O immer simplifica atualizações imutáveis com sintaxe mutável.

7. Integração com React e Next.js

Em componentes funcionais, o uso é direto com hooks. No Next.js, é importante evitar SSR para estado volátil:

// components/ContactForm.jsx
import { useState } from 'react';
import axios from 'axios';
import { create } from 'zustand';

const useFormStore = create((set) => ({
  name: '',
  email: '',
  message: '',
  setField: (field, value) => set({ [field]: value }),
  reset: () => set({ name: '', email: '', message: '' }),
}));

export default function ContactForm() {
  const { name, email, message, setField, reset } = useFormStore();
  const [submitting, setSubmitting] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setSubmitting(true);

    try {
      await axios.post('/api/contact', { name, email, message });
      reset();
      alert('Mensagem enviada com sucesso!');
    } catch (error) {
      alert('Erro ao enviar mensagem');
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={name}
        onChange={(e) => setField('name', e.target.value)}
        placeholder="Nome"
      />
      <input
        value={email}
        onChange={(e) => setField('email', e.target.value)}
        placeholder="Email"
      />
      <textarea
        value={message}
        onChange={(e) => setField('message', e.target.value)}
        placeholder="Mensagem"
      />
      <button type="submit" disabled={submitting}>
        {submitting ? 'Enviando...' : 'Enviar'}
      </button>
    </form>
  );
}

Em Next.js, stores que não precisam de SSR devem ser carregadas apenas no cliente:

// pages/contact.jsx
import dynamic from 'next/dynamic';

const ContactForm = dynamic(() => import('../components/ContactForm'), {
  ssr: false,
});

export default function ContactPage() {
  return (
    <div>
      <h1>Contato</h1>
      <ContactForm />
    </div>
  );
}

Zustand se destaca por sua simplicidade, performance e flexibilidade. Com uma API enxuta e suporte a middleware poderosos, é uma excelente escolha para gerenciamento de estado em aplicações React modernas, desde projetos pequenos até sistemas complexos com múltiplos domínios de estado.

Referências