Skill

Generate Robust Zustand Stores for React Apps

A skill for type-safe Zustand stores - Immer immutability, slice composition, persistence, and granular-subscription performance.

Works with githubreacttypescriptzustand

Maintainer of this project? Claim this page to edit the listing.


79
Spark score
out of 100
Updated 7 months ago
Version 1.0.0
Models

Add 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

01

Generate basic and advanced Zustand store structures.

02

Implement type-safe state management with TypeScript.

03

Apply advanced patterns like computed values, selectors, and store slicing.

04

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.