Skip to main content
Learn how to effectively define initial state and create actions that modify state in your Zustand stores.

Understanding State Structure

Create Zustand CLI uses a specific pattern for organizing stores:
const useStore = create((set) => ({
  // Initial state properties
  count: 0,
  user: null,
  isLoading: false,
  
  // Actions grouped in an object
  actions: {
    increment: () => set((state) => ({ count: state.count + 1 })),
    setUser: (user) => set({ user }),
    setLoading: (loading) => set({ isLoading: loading }),
  },
}));
This pattern separates state from actions, making your store more organized and easier to understand.

Defining Initial State

1
Simple State Values
2
When running the CLI, define simple state as JSON:
3
 Define initial state properties (as JSON):
{"count": 0, "message": "Hello", "isActive": true}
4
This generates:
5
const useStore = create((set) => ({
  count: 0,
  message: "Hello",
  isActive: true,
  actions: { /* ... */ },
}));
6
Nested Objects
7
Define nested state for complex data:
8
 Define initial state properties (as JSON):
{"user": {"name": "John", "age": 30}, "settings": {"theme": "dark", "notifications": true}}
9
This generates:
10
const useStore = create((set) => ({
  user: {
    name: "John",
    age: 30
  },
  settings: {
    theme: "dark",
    notifications: true
  },
  actions: { /* ... */ },
}));
11
Arrays
12
Initialize state with arrays:
13
 Define initial state properties (as JSON):
{"items": [], "tags": ["javascript", "react"]}
14
This generates:
15
const useStore = create((set) => ({
  items: [],
  tags: ["javascript", "react"],
  actions: { /* ... */ },
}));

Implementing Actions

The CLI generates action stubs that you need to implement:

Generated Action Stub

When you specify actions during CLI setup:
 Define actions (comma-separated):
increment,decrement,reset
The CLI generates:
actions: {
  increment: () => set((state) => ({})),
  decrement: () => set((state) => ({})),
  reset: () => set((state) => ({}))
}
The generated actions are empty stubs. You must implement the actual logic.

Simple State Updates

Implement actions that update a single value:
actions: {
  // Direct state update
  reset: () => set({ count: 0 }),
  
  // Update based on previous state
  increment: () => set((state) => ({ count: state.count + 1 })),
  
  // Update with parameter
  setCount: (value) => set({ count: value }),
}

Complex State Updates

Update nested objects or multiple properties:
actions: {
  updateUser: (name, age) => set((state) => ({
    user: {
      ...state.user,
      name,
      age,
    }
  })),
  
  updateSettings: (key, value) => set((state) => ({
    settings: {
      ...state.settings,
      [key]: value,
    }
  })),
}

Array Operations

Manipulate arrays in your state:
actions: {
  // Add item to array
  addItem: (item) => set((state) => ({
    items: [...state.items, item]
  })),
  
  // Remove item from array
  removeItem: (itemId) => set((state) => ({
    items: state.items.filter(item => item.id !== itemId)
  })),
  
  // Update item in array
  updateItem: (itemId, updates) => set((state) => ({
    items: state.items.map(item => 
      item.id === itemId ? { ...item, ...updates } : item
    )
  })),
  
  // Clear array
  clearItems: () => set({ items: [] }),
}

TypeScript Action Types

When using TypeScript, define proper types for your actions:

Update Generated Types

The CLI generates basic action types:
interface State {
  count: number;
  user: { name: string; age: number } | null;
  actions: {
    increment: () => void;  // Generated
    setUser: () => void;    // Generated
  };
}
Update with parameter types:
interface User {
  name: string;
  age: number;
}

interface State {
  count: number;
  user: User | null;
  actions: {
    increment: () => void;
    decrement: () => void;
    setCount: (value: number) => void;
    setUser: (user: User) => void;
    updateUser: (updates: Partial<User>) => void;
  };
}

Action Patterns

Async Actions

Handle asynchronous operations:
actions: {
  fetchUser: async (userId) => {
    set({ isLoading: true });
    try {
      const response = await fetch(`/api/users/${userId}`);
      const user = await response.json();
      set({ user, isLoading: false });
    } catch (error) {
      set({ error: error.message, isLoading: false });
    }
  },
}

Dependent Actions

Actions that call other actions:
const useStore = create((set, get) => ({
  count: 0,
  history: [],
  actions: {
    increment: () => {
      set((state) => ({ count: state.count + 1 }));
      get().actions.addToHistory('increment');
    },
    addToHistory: (action) => set((state) => ({
      history: [...state.history, { action, timestamp: Date.now() }]
    })),
  },
}));

Reset Actions

Reset state to initial values:
const initialState = {
  count: 0,
  user: null,
  isLoading: false,
};

const useStore = create((set) => ({
  ...initialState,
  actions: {
    // ... other actions
    reset: () => set(initialState),
  },
}));

Best Practices

Each action should do one thing well. If an action is getting complex, split it into multiple actions.
// Good
actions: {
  setLoading: (loading) => set({ isLoading: loading }),
  setError: (error) => set({ error }),
  setData: (data) => set({ data }),
}

// Avoid
actions: {
  fetchData: async () => {
    // Too much logic in one action
  }
}
Action names should clearly describe what they do.
// Good
actions: {
  addItemToCart: (item) => { /* ... */ },
  removeItemFromCart: (itemId) => { /* ... */ },
  clearCart: () => { /* ... */ },
}

// Avoid
actions: {
  add: (item) => { /* ... */ },
  remove: (id) => { /* ... */ },
  clear: () => { /* ... */ },
}
Always return new objects/arrays instead of mutating existing state.
// Good
addItem: (item) => set((state) => ({
  items: [...state.items, item]
})),

// Avoid
addItem: (item) => set((state) => {
  state.items.push(item); // Mutation!
  return { items: state.items };
}),

Next Steps

Basic Store

Create your first store

TypeScript Store

Add type safety

Build docs developers (and LLMs) love