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
When running the CLI, define simple state as JSON:
➤ Define initial state properties (as JSON):
{"count": 0, "message": "Hello", "isActive": true}
const useStore = create((set) => ({
count: 0,
message: "Hello",
isActive: true,
actions: { /* ... */ },
}));
Define nested state for complex data:
➤ Define initial state properties (as JSON):
{"user": {"name": "John", "age": 30}, "settings": {"theme": "dark", "notifications": true}}
const useStore = create((set) => ({
user: {
name: "John",
age: 30
},
settings: {
theme: "dark",
notifications: true
},
actions: { /* ... */ },
}));
Initialize state with arrays:
➤ Define initial state properties (as JSON):
{"items": [], "tags": ["javascript", "react"]}
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