---
description: Best practices for using Zustand for state management
globs:
alwaysApply: false
---
# Zustand Best Practices
Best practices for using Zustand for state management. Auto-included for store files.
<rule>
name: zustand_best_practices
description: Best practices for using Zustand for state management. Auto-included for store files.
globs: ["**/*store*.{ts,tsx,js,jsx}"]
filters:
- type: content
pattern: "(?:import|from).*zustand"
actions:
- type: suggest
message: |
1. **Keep Stores Small and Focused**:
- Unlike Redux, prefer multiple small stores over a single global store
- Each store should be responsible for a single piece of functionality
- Avoid storing unrelated state in the same store
- For larger applications, create multiple small, focused stores instead of a single large one
- This improves organization, reduces errors, and simplifies testing
2. **Structure Store with Separated State and Actions Objects**:
- Clearly separate state and actions into their own objects within the store
- State should be a single object containing all state properties
- Actions should be grouped in their own object
- This pattern enables better organization and selective exports
3. `// ✅ Good: Clear separation of state and actions
const useStore = create((set, get) => {
// State object
const state = {
count: 0,
items: [],
}
// Actions object with all functions that modify state
const actions = {
increment: () => set((state) => ({ count: state.count + 1 })),
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
resetAll: () => set({ count: 0, items: [] }),
}
return {
...state,
actions,
}
})
`
4. **Create Atomic State Hooks and a Single Actions Hook**:
- Export individual atomic hooks for each state property
- Export a single hook for all actions
- This prevents unnecessary re-renders while maintaining easy access to all actions
5. `// Store implementation
const useStore = create((set, get) => {
const state = {
bears: 0,
fish: 0,
}
const actions = {
increaseBears: () => set((state) => ({ bears: state.bears + 1 })),
increaseFish: () => set((state) => ({ fish: state.fish + 1 })),
feedBear: () => {
const { bears, fish } = get()
if (fish > 0) {
set({ bears: bears + 1, fish: fish - 1 })
}
},
resetAnimals: () => set({ bears: 0, fish: 0 }),
}
return {
...state,
actions,
}
})
// ✅ Good: Atomic selectors for state
export const useBears = () => useStore((state) => state.bears)
export const useFish = () => useStore((state) => state.fish)
// ✅ Good: Single hook for all actions
export const useStoreActions = () => useStore((state) => state.actions)
// 🤔 Optional: If you need to split actions into groups
import shallow from 'zustand/shallow'
export const useBearActions = () => useStore(
(state) => ({
increaseBears: state.actions.increaseBears,
feedBear: state.actions.feedBear
}),
shallow
)
`
6. **Use **`shallow`** for Multi-Property State Selections**:
- When you need to select multiple state properties, use `shallow` to prevent unnecessary re-renders
- Import `shallow` from 'zustand/shallow' (or 'zustand/react/shallow' in newer versions)
- This performs a shallow comparison of the selected object's properties instead of reference equality
- Perfect for creating domain-specific hooks that use multiple state values
7. `// Without shallow - will re-render on ANY state change
// 🚨 Bad: Component re-renders when unrelated state changes
const UserProfile = () => {
const { name, email, avatar } = useUserStore((state) => ({
name: state.name,
email: state.email,
avatar: state.avatar
}))
// Will re-render even if other unrelated state changes
}
// With shallow - only re-renders when selected properties change
// ✅ Good: Using shallow for multi-property selection
import { shallow } from 'zustand/shallow'
const UserProfile = () => {
const { name, email, avatar } = useUserStore(
(state) => ({
name: state.name,
email: state.email,
avatar: state.avatar
}),
shallow
)
// Only re-renders when name, email, or avatar change
}
// ✅ Good: Create domain-specific hooks with shallow
export const useUserProfileData = () => useUserStore(
(state) => ({
name: state.name,
email: state.email,
avatar: state.avatar,
isVerified: state.isVerified
}),
shallow
)
// ✅ Alternative syntax - array selector with shallow
const [name, email] = useUserStore(
(state) => [state.name, state.email],
shallow
)
`
8. **Encapsulate Business Logic Within Store Actions**:
- All business logic should live inside the store's actions, not outside
- Actions should model domain events with all necessary transformations
- Components should only need to call actions directly, not perform business logic
9. `// 🚨 Bad: Business logic outside the store
const useTodoStore = create((set) => ({
todos: [],
setTodos: (todos) => set({ todos }),
}))
// Logic exists outside the store - BAD!
function addTodo(todo) {
const formattedTodo = {
...todo,
id: Date.now(),
createdAt: new Date().toISOString(),
completed: false
}
const todos = useTodoStore.getState().todos
useTodoStore.getState().setTodos([...todos, formattedTodo])
}
// ✅ Good: Business logic inside store actions
const useTodoStore = create((set, get) => {
const state = {
todos: [],
}
const actions = {
addTodo: (todo) => set((state) => ({
todos: [...state.todos, {
...todo,
id: Date.now(),
createdAt: new Date().toISOString(),
completed: false
}]
})),
toggleTodo: (id) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
})),
clearCompletedTodos: () => set((state) => ({
todos: state.todos.filter(todo => !todo.completed)
})),
}
return {
...state,
actions,
}
})
// Usage in component is simple - just call the action
function TodoForm() {
const { addTodo } = useStoreActions()
const handleSubmit = (e) => {
e.preventDefault()
addTodo({ title: e.target.todo.value })
e.target.reset()
}
return <form onSubmit={handleSubmit}>...</form>
}
`
10. **Use Immutable Updates**:
- Always update state immutably
- Consider using immer for complex nested state updates
11. `// With immer middleware
import { immer } from 'zustand/middleware/immer'
const useStore = create(
immer((set) => ({
nested: { structure: { value: 0 } },
actions: {
updateNested: () => set((state) => {
state.nested.structure.value += 1
})
}
}))
)
`
12. **Type Your Stores**:
- Split types into State, Actions, and Store
- This improves clarity and type safety
13. `interface TodoState {
todos: Todo[]
}
interface TodoActions {
addTodo: (todo: Omit<Todo, 'id' | 'createdAt' | 'completed'>) => void
toggleTodo: (id: number) => void
clearCompletedTodos: () => void
}
// The store combines state and the actions object
interface TodoStore extends TodoState {
actions: TodoActions
}
const useTodoStore = create<TodoStore>((set) => {
const state: TodoState = {
todos: [],
}
const actions: TodoActions = {
addTodo: (todo) => set((state) => ({
todos: [...state.todos, {
...todo,
id: Date.now(),
createdAt: new Date().toISOString(),
completed: false
}]
})),
toggleTodo: (id) => set((state) => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
})),
clearCompletedTodos: () => set((state) => ({
todos: state.todos.filter(todo => !todo.completed)
})),
}
return {
...state,
actions,
}
})
`
examples:
- input: |
// Bad: Everything in one big store
const useGlobalStore = create((set) => ({
user: null,
posts: [],
comments: [],
likes: {},
notifications: [],
settings: {},
// many more unrelated pieces of state...
}))
- // Good: Separate stores for separate concerns
const useUserStore = create((set) => ({
user: null,
actions: {
login: (user) => set({ user }),
logout: () => set({ user: null }),
}
}))
- const usePostsStore = create((set) => ({
posts: [],
actions: {
addPost: (post) => set((state) => ({
posts: [...state.posts, post]
})),
}
}))
output: "Split large stores into smaller, more focused ones"
- input: |
// Bad: No clear separation of state and actions
const useBadStore = create((set) => ({
bears: 0,
fish: 0,
increaseBears: () => set((state) => ({ bears: state.bears + 1 })),
increaseFish: () => set((state) => ({ fish: state.fish + 1 })),
}))
- // Bad: Using the store without atomic selectors
function Component() {
const { bears, increaseBears } = useBadStore()
return <div onClick={increaseBears}>{bears}</div>
}
- // Good: Clear separation and atomic selectors
const useGoodStore = create((set) => {
const state = {
bears: 0,
fish: 0,
}
- `const actions = {
increaseBears: () => set((state) => ({ bears: state.bears + 1 })),
increaseFish: () => set((state) => ({ fish: state.fish + 1 })),
}
return { ...state, actions }
`
- })
- // Export atomic state selectors
export const useBears = () => useGoodStore((state) => state.bears)
export const useFish = () => useGoodStore((state) => state.fish)
- // Export actions as a single hook
export const useAnimalActions = () => useGoodStore((state) => state.actions)
- // Good usage in components
function BetterComponent() {
const bears = useBears()
const { increaseBears } = useAnimalActions()
- `return <div onClick={increaseBears}>{bears}</div>
`
- }
output: "Use atomic state selectors and group actions"
- input: |
// Bad: Business logic outside the store
const useCartStore = create((set) => ({
items: [],
setItems: (items) => set({ items }),
}))
- // Business logic in a component or utility
function addToCart(product, quantity = 1) {
const cart = useCartStore.getState()
const existingItem = cart.items.find(item => item.id === product.id)
- `if (existingItem) {
const updatedItems = cart.items.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + quantity }
: item
)
cart.setItems(updatedItems)
} else {
cart.setItems([...cart.items, { ...product, quantity }])
}
`
- }
- // Good: Business logic inside store actions
const useCartStore = create((set, get) => {
const state = {
items: [],
}
- `const actions = {
addToCart: (product, quantity = 1) => set((state) => {
const existingItem = state.items.find(item => item.id === product.id)
if (existingItem) {
return {
items: state.items.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + quantity }
: item
)
}
} else {
return {
items: [...state.items, { ...product, quantity }]
}
}
}),
removeFromCart: (productId) => set((state) => ({
items: state.items.filter(item => item.id !== productId)
})),
clearCart: () => set({ items: [] }),
}
return {
...state,
actions,
}
`
- })
- // Export selectors and actions
export const useCartItems = () => useCartStore((state) => state.items)
export const useCartTotal = () => useCartStore((state) =>
state.items.reduce((total, item) => total + (item.price * item.quantity), 0)
)
export const useCartActions = () => useCartStore((state) => state.actions)
- // Usage in component
function AddToCartButton({ product }) {
const { addToCart } = useCartActions()
return <button onClick={() => addToCart(product)}>Add to Cart</button>
}
output: "Encapsulate business logic within store actions"
- input: |
// Bad: Multi-property selection without shallow (causes unnecessary re-renders)
const ProfileComponent = () => {
// This will re-render even if unrelated state changes
const { firstName, lastName, email } = useUserStore(state => ({
firstName: state.firstName,
lastName: state.lastName,
email: state.email
}))
- `return <div>{firstName} {lastName}: {email}</div>
`
- }
- // Good: Multi-property selection with shallow
import { shallow } from 'zustand/shallow'
- const ProfileComponent = () => {
// This only re-renders when firstName, lastName, or email change
const { firstName, lastName, email } = useUserStore(
state => ({
firstName: state.firstName,
lastName: state.lastName,
email: state.email
}),
shallow
)
- `return <div>{firstName} {lastName}: {email}</div>
`
- }
- // Even better: Domain-specific hook with shallow
export const useUserProfile = () => useUserStore(
state => ({
firstName: state.firstName,
lastName: state.lastName,
email: state.email,
fullName: `${state.firstName} ${state.lastName}`
}),
shallow
)
- // Usage in component
const ProfileComponent = () => {
const { firstName, lastName, email, fullName } = useUserProfile()
return <div>{fullName}: {email}</div>
}
output: "Use shallow for selecting multiple state properties efficiently"
metadata:
priority: high
version: 1.2
</rule>