Tipando stores do Zustand
1. Introdução ao Zustand com TypeScript
O Zustand é uma biblioteca de gerenciamento de estado para React que se destaca pela simplicidade e performance. Quando combinado com TypeScript, ele oferece tipagem forte que previne erros comuns em tempo de compilação, como acessar propriedades inexistentes ou chamar ações com argumentos incorretos.
Benefícios da tipagem forte em stores globais:
- Autocomplete inteligente no editor
- Detecção precoce de erros de tipo
- Refatoração segura de estado e ações
- Documentação viva através dos tipos
Instalação:
npm install zustand
# ou
yarn add zustand
Diferente do Redux, o Zustand não requer configuração de providers ou action creators, tornando-o ideal para projetos TypeScript onde a simplicidade é prioridade.
2. Criando a primeira store tipada
Vamos criar uma store de carrinho de compras com tipagem completa:
import { create } from 'zustand'
interface CartItem {
id: number
name: string
price: number
quantity: number
}
interface CartState {
items: CartItem[]
total: number
addItem: (item: Omit<CartItem, 'quantity'>) => void
removeItem: (id: number) => void
clearCart: () => void
}
const useCartStore = create<CartState>()((set) => ({
items: [],
total: 0,
addItem: (item) => set((state) => {
const existingItem = state.items.find(i => i.id === item.id)
if (existingItem) {
return {
items: state.items.map(i =>
i.id === item.id
? { ...i, quantity: i.quantity + 1 }
: i
),
total: state.total + item.price
}
}
return {
items: [...state.items, { ...item, quantity: 1 }],
total: state.total + item.price
}
}),
removeItem: (id) => set((state) => ({
items: state.items.filter(i => i.id !== id),
total: state.items
.filter(i => i.id !== id)
.reduce((acc, i) => acc + i.price * i.quantity, 0)
})),
clearCart: () => set({ items: [], total: 0 })
}))
Note o uso de create<CartState>() com parênteses duplos — isso é necessário para inferência correta de tipos no TypeScript.
3. Tipagem de ações e mutações complexas
Ações assíncronas são comuns em aplicações reais. Veja como tipá-las corretamente:
interface UserState {
user: User | null
loading: boolean
error: string | null
fetchUser: (id: number) => Promise<void>
updateUser: (data: Partial<User>) => Promise<void>
}
const useUserStore = create<UserState>()((set, get) => ({
user: null,
loading: false,
error: null,
fetchUser: async (id) => {
set({ loading: true, error: null })
try {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) throw new Error('Failed to fetch')
const user: User = await response.json()
set({ user, loading: false })
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Unknown error',
loading: false
})
}
},
updateUser: async (data) => {
const currentUser = get().user
if (!currentUser) return
set({ loading: true })
try {
const response = await fetch(`/api/users/${currentUser.id}`, {
method: 'PATCH',
body: JSON.stringify(data)
})
const updatedUser: User = await response.json()
set({ user: updatedUser, loading: false })
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Update failed',
loading: false
})
}
}
}))
O get() permite acessar o estado atual dentro de ações, essencial para lógicas que dependem do estado anterior.
4. Utilizando slices para modularizar stores
Para stores grandes, use slices para organizar o código:
import { create, StateCreator } from 'zustand'
interface AuthSlice {
user: User | null
token: string | null
login: (email: string, password: string) => Promise<void>
logout: () => void
}
interface CartSlice {
items: CartItem[]
addToCart: (item: CartItem) => void
}
interface AppStore extends AuthSlice, CartSlice {}
const createAuthSlice: StateCreator<AppStore, [], [], AuthSlice> = (set) => ({
user: null,
token: null,
login: async (email, password) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
})
const data = await response.json()
set({ user: data.user, token: data.token })
},
logout: () => set({ user: null, token: null })
})
const createCartSlice: StateCreator<AppStore, [], [], CartSlice> = (set) => ({
items: [],
addToCart: (item) => set((state) => ({
items: [...state.items, item]
}))
})
const useAppStore = create<AppStore>()((...a) => ({
...createAuthSlice(...a),
...createCartSlice(...a)
}))
A tipagem StateCreator garante que os slices compartilhem o mesmo tipo de store, permitindo acesso cruzado entre eles.
5. Middlewares com tipagem avançada
O middleware persist com tipagem parcial:
import { persist, createJSONStorage } from 'zustand/middleware'
interface SettingsState {
theme: 'light' | 'dark'
language: string
setTheme: (theme: 'light' | 'dark') => void
setLanguage: (lang: string) => void
}
const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
theme: 'light',
language: 'en',
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language })
}),
{
name: 'settings-storage',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
theme: state.theme,
language: state.language
})
}
)
)
Middleware immer para mutações imutáveis:
import { immer } from 'zustand/middleware/immer'
interface TodoState {
todos: Todo[]
addTodo: (title: string) => void
toggleTodo: (id: number) => void
}
const useTodoStore = create<TodoState>()(
immer((set) => ({
todos: [],
addTodo: (title) => set((state) => {
state.todos.push({ id: Date.now(), title, completed: false })
}),
toggleTodo: (id) => set((state) => {
const todo = state.todos.find(t => t.id === id)
if (todo) todo.completed = !todo.completed
})
}))
)
6. Integração com hooks do React
Hooks customizados com seletores tipados:
import { useStore } from 'zustand'
import { useCartStore } from './cart-store'
// Hook com seletor parcial
function useCartTotal() {
return useCartStore((state) => state.total)
}
// Hook customizado completo
function useUserStore() {
const user = useStore(useUserStore, (state) => state.user)
const loading = useStore(useUserStore, (state) => state.loading)
const fetchUser = useStore(useUserStore, (state) => state.fetchUser)
return { user, loading, fetchUser }
}
// Subscribe para reatividade fina
useCartStore.subscribe(
(state) => state.items.length,
(length) => console.log(`Cart has ${length} items`)
)
7. Boas práticas e padrões avançados
Usando type vs interface:
// Prefira interface para objetos que podem ser estendidos
interface AppState {
user: User | null
}
// Use type para unions ou tipos complexos
type Status = 'idle' | 'loading' | 'success' | 'error'
type AsyncState<T> = {
data: T | null
status: Status
error: string | null
}
Evitando any com unknown:
// Ruim
const updateItem = (item: any) => set({ item })
// Bom
const updateItem = <T>(item: T) => set({ item: item as unknown })
Testando stores tipadas com vitest:
import { describe, it, expect } from 'vitest'
import { useCartStore } from './cart-store'
describe('Cart Store', () => {
beforeEach(() => {
useCartStore.setState({ items: [], total: 0 })
})
it('should add item to cart', () => {
useCartStore.getState().addItem({ id: 1, name: 'Book', price: 29.90 })
const state = useCartStore.getState()
expect(state.items).toHaveLength(1)
expect(state.total).toBe(29.90)
})
it('should increase quantity for existing item', () => {
useCartStore.getState().addItem({ id: 1, name: 'Book', price: 29.90 })
useCartStore.getState().addItem({ id: 1, name: 'Book', price: 29.90 })
const state = useCartStore.getState()
expect(state.items[0].quantity).toBe(2)
expect(state.total).toBe(59.80)
})
})
A tipagem forte do Zustand com TypeScript transforma o gerenciamento de estado em uma experiência segura e produtiva. Ao adotar esses padrões, você reduzirá bugs, melhorará a manutenibilidade e proporcionará uma melhor experiência de desenvolvimento para sua equipe.
Referências
- Documentação oficial do Zustand — Guia completo de uso do Zustand, incluindo integração com TypeScript
- Zustand com TypeScript: Guia prático — Tutorial detalhado sobre tipagem de stores no Zustand
- Zustand Middlewares: Persist e Immer — Documentação oficial sobre middlewares com exemplos de tipagem
- TypeScript Deep Dive: Zustand Patterns — Padrões avançados de TypeScript aplicados ao Zustand
- Testing Zustand Stores with Vitest — Guia de testes para stores Zustand usando Vitest e React Testing Library