Generate Robust Zustand Stores for React Apps
A skill for type-safe Zustand stores - Immer immutability, slice composition, persistence, and granular-subscription performance.
Maintainer of this project? Claim this page to edit the listing.
1.0.0Add to Favorites
Why it matters
Build highly performant, type-safe, and maintainable state management solutions for React applications using Zustand. This asset specializes in creating robust stores with advanced patterns, ensuring immutability, modularity, and efficient state updates.
Outcomes
What it gets done
Generate basic and advanced Zustand store structures.
Implement type-safe state management with TypeScript.
Apply advanced patterns like computed values, selectors, and store slicing.
Integrate persistence, middleware, and devtools for enhanced functionality.
Install
Add it to your toolbox
Run in your project directory:
curl -fsSL https://spark.entire.vc/get/vb-zustand-store-builder | bash Overview
Zustand Store Builder
This skill builds type-safe Zustand stores with Immer-based immutability, slice composition, strategic localStorage persistence, and granular-subscription performance tuning. Use it when building shared React application state that needs typed actions, computed selectors, and controlled re-renders, not local-only component state.
What it does
This skill builds type-safe, performant Zustand stores for React state management, following five core principles: a single source of truth per domain, immutable updates via the Immer middleware or manual patterns, full TypeScript inference, performance tuned through selector design and state structure, and modular, composable, testable stores. A basic store is created with create<State>() wrapped in the immer middleware, letting actions write mutative-looking code (state.todos.push(...)) that Immer converts into immutable updates under the hood.
When to use - and when NOT to
Use it when building application state that needs typed actions, computed selectors, and controlled re-renders - not local component state that never needs to be shared. It is not meant to store derived values: computing filtered lists or stats from a selector at read time, rather than storing them redundantly in the store, is the pattern the skill itself follows and one of its named anti-patterns to avoid violating.
Inputs and outputs
Given application state requirements, it produces reusable selectors defined outside the store (a filter-switching selector and a stats-aggregating selector, both composed cleanly in components), sliced and composed stores (independent UserSlice and NotificationSlice interfaces merged into one AppState type, with cross-slice actions - a login action that calls get().addNotification(...) to trigger a success or error notification from within the user slice), persisted stores (the persist middleware backed by localStorage via createJSONStorage, with partialize selecting exactly which state fields survive a reload), and a testable store factory pattern that accepts partial initial state for dependency injection in tests.
import { persist, createJSONStorage } from 'zustand/middleware'
const useSettingsStore = create<SettingsState>()((
persist(
immer((set) => ({
theme: 'light',
language: 'en',
notifications: {
email: true,
push: true,
desktop: false
},
updateTheme: (theme) => set((state) => {
state.theme = theme
}),
updateNotificationSettings: (settings) => set((state) => {
Object.assign(state.notifications, settings)
})
})),
{
name: 'app-settings',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
theme: state.theme,
language: state.language,
notifications: state.notifications
})
}
)
))
Integrations
Performance-sensitive reads subscribe to a narrow slice of state directly (state.todos.length) rather than the whole object, or use shallow comparison when selecting an object of multiple fields together to avoid unnecessary re-renders. Debugging wires in devtools around persist and immer, with tracing and serialization options exposed to the Redux DevTools extension.
Who it's for
React developers building shared application state who need the full pattern set - typed stores, Immer-based immutability, reusable selectors, slice composition, strategic persistence, and testable store factories - while explicitly avoiding named anti-patterns: storing derived state instead of computing it, building one monolithic store, mutating state directly without Immer, over-subscribing to the whole store, and mixing UI state with business logic.
Source README
Zustand Store Builder Expert
You are an expert in building robust, performant Zustand stores for React applications. You specialize in creating type-safe, maintainable state management solutions using Zustand's powerful yet simple API, including advanced patterns for complex applications.
Core Principles
- Single Source of Truth: Design stores that serve as the definitive source for application state
- Immutability: Always use immutable updates using Immer integration or manual immutable patterns
- Type Safety: Leverage TypeScript to create fully typed stores with proper inference
- Performance: Minimize re-renders through proper selector usage and state structure
- Modularity: Create composable stores that can be easily tested and maintained
Basic Store Creation
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
interface TodoState {
todos: Todo[]
filter: 'all' | 'completed' | 'active'
addTodo: (text: string) => void
toggleTodo: (id: string) => void
setFilter: (filter: TodoState['filter']) => void
}
const useTodoStore = create<TodoState>()((
immer((set) => ({
todos: [],
filter: 'all',
addTodo: (text) => set((state) => {
state.todos.push({
id: crypto.randomUUID(),
text,
completed: false,
createdAt: new Date()
})
}),
toggleTodo: (id) => set((state) => {
const todo = state.todos.find(t => t.id === id)
if (todo) todo.completed = !todo.completed
}),
setFilter: (filter) => set({ filter })
}))
))
Advanced Store Patterns
Computed Values and Selectors
// Define selectors outside the store for reusability
export const selectFilteredTodos = (state: TodoState) => {
switch (state.filter) {
case 'completed':
return state.todos.filter(todo => todo.completed)
case 'active':
return state.todos.filter(todo => !todo.completed)
default:
return state.todos
}
}
export const selectTodoStats = (state: TodoState) => ({
total: state.todos.length,
completed: state.todos.filter(t => t.completed).length,
active: state.todos.filter(t => !t.completed).length
})
// Usage in components with proper memoization
const TodoList = () => {
const filteredTodos = useTodoStore(selectFilteredTodos)
const stats = useTodoStore(selectTodoStats)
return (
<div>
<div>Active: {stats.active}, Completed: {stats.completed}</div>
{filteredTodos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
</div>
)
}
Store Slicing and Composition
// Create focused slices for better organization
interface UserSlice {
user: User | null
login: (credentials: LoginCredentials) => Promise<void>
logout: () => void
}
interface NotificationSlice {
notifications: Notification[]
addNotification: (notification: Omit<Notification, 'id'>) => void
removeNotification: (id: string) => void
}
type AppState = UserSlice & NotificationSlice
const useAppStore = create<AppState>()((
immer((set, get) => ({
// User slice
user: null,
login: async (credentials) => {
try {
const user = await authService.login(credentials)
set((state) => { state.user = user })
get().addNotification({
type: 'success',
message: `Welcome back, ${user.name}!`
})
} catch (error) {
get().addNotification({
type: 'error',
message: 'Login failed'
})
}
},
logout: () => set((state) => {
state.user = null
}),
// Notification slice
notifications: [],
addNotification: (notification) => set((state) => {
state.notifications.push({
...notification,
id: crypto.randomUUID(),
timestamp: Date.now()
})
}),
removeNotification: (id) => set((state) => {
state.notifications = state.notifications.filter(n => n.id !== id)
})
}))
))
Persistence and Middleware
import { persist, createJSONStorage } from 'zustand/middleware'
const useSettingsStore = create<SettingsState>()((
persist(
immer((set) => ({
theme: 'light',
language: 'en',
notifications: {
email: true,
push: true,
desktop: false
},
updateTheme: (theme) => set((state) => {
state.theme = theme
}),
updateNotificationSettings: (settings) => set((state) => {
Object.assign(state.notifications, settings)
})
})),
{
name: 'app-settings',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
theme: state.theme,
language: state.language,
notifications: state.notifications
})
}
)
))
Testing Strategies
// Create testable store factory
export const createTodoStore = (initialState?: Partial<TodoState>) =>
create<TodoState>()((
immer((set) => ({
todos: [],
filter: 'all',
...initialState,
addTodo: (text) => set((state) => {
state.todos.push({ id: crypto.randomUUID(), text, completed: false })
}),
// ... other actions
}))
))
// Test example
const mockStore = createTodoStore({ todos: mockTodos })
const { result } = renderHook(() => mockStore(selectFilteredTodos))
expect(result.current).toHaveLength(2)
Performance Optimization
Granular Subscriptions
// Subscribe only to specific state slices
const TodoCounter = () => {
const todoCount = useTodoStore(state => state.todos.length)
return <span>Total: {todoCount}</span>
}
// Use shallow comparison for object selections
import { shallow } from 'zustand/shallow'
const TodoFilters = () => {
const { filter, setFilter } = useTodoStore(
state => ({ filter: state.filter, setFilter: state.setFilter }),
shallow
)
return (
<select value={filter} onChange={e => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
</select>
)
}
DevTools Integration
import { devtools } from 'zustand/middleware'
const useStore = create<State>()((
devtools(
persist(
immer((set, get) => ({
// store implementation
})),
{ name: 'app-storage' }
),
{
name: 'app-store',
trace: true,
serialize: { options: true }
}
)
))
Best Practices
- Use TypeScript: Always type your stores for better DX and fewer bugs
- Leverage Immer: Use the immer middleware for cleaner mutation syntax
- Create Focused Selectors: Extract reusable selectors to minimize re-renders
- Persist Strategically: Only persist necessary state and use partialize
- Structure Actions Logically: Group related actions and use descriptive names
- Handle Async Properly: Use proper error handling in async actions
- Test Store Logic: Create testable stores with dependency injection patterns
- Monitor Performance: Use React DevTools Profiler to identify unnecessary re-renders
Common Anti-patterns to Avoid
- Storing derived state instead of computing it
- Creating overly large, monolithic stores
- Mutating state directly without Immer
- Subscribing to entire store when only small slices are needed
- Mixing UI state with business logic inappropriately
FAQ
Common questions
Discussion
Questions & comments ยท 0
Sign In Sign in to leave a comment.