Rui Tao's Portfolio

Mastering Zustand State Management in React Applications

Graphs of performance analytics on a laptop screen
Published on
/6 mins read/---

Introduction

State management is a crucial aspect of modern React applications. While there are many state management solutions available, Zustand has gained popularity due to its simplicity, flexibility, and performance. In this article, we'll explore different ways to use Zustand effectively in your React applications.

Understanding Zustand

Zustand is a small, fast, and scalable state management solution. It provides a simple yet powerful API that can be used both inside and outside of React components. Let's explore the different approaches to using Zustand.

Usage Patterns

1. Hook-based Usage (Inside React Components)

The most common way to use Zustand is through its hook API within React components:

import create from 'zustand';
 
// Define your store
interface StoreState {
    count: number;
    increment: () => void;
    decrement: () => void;
}
 
const useStore = create<StoreState>((set) => ({
    count: 0,
    increment: () => set((state) => ({ count: state.count + 1 })),
    decrement: () => set((state) => ({ count: state.count - 1 }))
}));
 
// Using the entire store
const CounterComponent: React.FC = () => {
    const state = useStore();
    return (
        <div>
            <p>Count: {state.count}</p>
            <button onClick={state.increment}>+</button>
            <button onClick={state.decrement}>-</button>
        </div>
    );
};

2. Using Selectors for Performance

For better performance, use selectors to subscribe to specific parts of the store:

interface TodoStore {
    todos: Todo[];
    filter: 'all' | 'active' | 'completed';
    addTodo: (text: string) => void;
    toggleTodo: (id: string) => void;
}
 
const useTodoStore = create<TodoStore>((set) => ({
    todos: [],
    filter: 'all',
    addTodo: (text) => set((state) => ({
        todos: [...state.todos, { id: Date.now(), text, completed: false }]
    })),
    toggleTodo: (id) => set((state) => ({
        todos: state.todos.map(todo =>
            todo.id === id ? { ...todo, completed: !todo.completed } : todo
        )
    }))
}));
 
// Using selectors
const TodoList: React.FC = () => {
    // Only re-renders when todos change
    const todos = useTodoStore((state) => state.todos);
    // Only re-renders when filter changes
    const filter = useTodoStore((state) => state.filter);
    
    const filteredTodos = useMemo(() => {
        switch (filter) {
            case 'active':
                return todos.filter(todo => !todo.completed);
            case 'completed':
                return todos.filter(todo => todo.completed);
            default:
                return todos;
        }
    }, [todos, filter]);
 
    return (
        <ul>
            {filteredTodos.map(todo => (
                <TodoItem key={todo.id} todo={todo} />
            ))}
        </ul>
    );
};

3. Accessing State Outside React

Sometimes you need to access or update state outside of React components:

interface AuthStore {
    user: User | null;
    token: string | null;
    login: (credentials: Credentials) => Promise<void>;
    logout: () => void;
}
 
const useAuthStore = create<AuthStore>((set) => ({
    user: null,
    token: null,
    login: async (credentials) => {
        const response = await api.login(credentials);
        set({ user: response.user, token: response.token });
    },
    logout: () => set({ user: null, token: null })
}));
 
// Utility function outside React
const setupAxiosInterceptors = () => {
    axios.interceptors.request.use((config) => {
        const token = useAuthStore.getState().token;
        if (token) {
            config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
    });
};
 
// Subscribe to state changes
const unsubscribe = useAuthStore.subscribe(
    (state) => state.token,
    (token) => {
        if (token) {
            localStorage.setItem('token', token);
        } else {
            localStorage.removeItem('token');
        }
    }
);

4. Hybrid Approach

Sometimes you might need the latest state value without causing re-renders:

const ChatComponent: React.FC = () => {
    const sendMessage = useCallback(async (text: string) => {
        // Get latest state without subscription
        const { user } = useAuthStore.getState();
        if (!user) return;
 
        await api.sendMessage({
            text,
            userId: user.id,
            timestamp: Date.now()
        });
    }, []);
 
    return <MessageInput onSend={sendMessage} />;
};

Best Practices

1. Store Organization

Keep your stores focused and organized:

// Split into multiple stores
const useUserStore = create<UserStore>((set) => ({
    user: null,
    setUser: (user) => set({ user })
}));
 
const useSettingsStore = create<SettingsStore>((set) => ({
    theme: 'light',
    language: 'en',
    updateSettings: (settings) => set(settings)
}));
 
// Combine stores when needed
const useAppStore = () => {
    const user = useUserStore((state) => state.user);
    const settings = useSettingsStore((state) => ({
        theme: state.theme,
        language: state.language
    }));
 
    return { user, settings };
};

2. TypeScript Integration

Leverage TypeScript for better type safety:

interface StoreState {
    count: number;
    increment: () => void;
    decrement: () => void;
}
 
interface StoreActions {
    reset: () => void;
}
 
const useStore = create<StoreState & StoreActions>((set) => ({
    count: 0,
    increment: () => set((state) => ({ count: state.count + 1 })),
    decrement: () => set((state) => ({ count: state.count - 1 })),
    reset: () => set({ count: 0 })
}));

3. Performance Optimization

Use middleware for debugging and optimization:

import { devtools, persist } from 'zustand/middleware';
 
const useStore = create<StoreState>()(
    devtools(
        persist(
            (set) => ({
                count: 0,
                increment: () => set((state) => ({ count: state.count + 1 })),
                decrement: () => set((state) => ({ count: state.count - 1 }))
            }),
            {
                name: 'app-storage',
                getStorage: () => localStorage
            }
        )
    )
);

Advanced Patterns

1. Computed Values

Implement computed values using selectors:

interface TodoStore {
    todos: Todo[];
    getCompletedCount: () => number;
    getActiveCount: () => number;
}
 
const useTodoStore = create<TodoStore>((set, get) => ({
    todos: [],
    getCompletedCount: () => get().todos.filter(t => t.completed).length,
    getActiveCount: () => get().todos.filter(t => !t.completed).length
}));
 
// Usage with memoization
const TodoStats: React.FC = () => {
    const completedCount = useTodoStore((state) => state.getCompletedCount());
    const activeCount = useTodoStore((state) => state.getActiveCount());
 
    return (
        <div>
            <p>Completed: {completedCount}</p>
            <p>Active: {activeCount}</p>
        </div>
    );
};

2. Async Actions

Handle async operations gracefully:

interface DataStore {
    data: Data[];
    loading: boolean;
    error: Error | null;
    fetchData: () => Promise<void>;
}
 
const useDataStore = create<DataStore>((set) => ({
    data: [],
    loading: false,
    error: null,
    fetchData: async () => {
        set({ loading: true, error: null });
        try {
            const response = await api.fetchData();
            set({ data: response, loading: false });
        } catch (error) {
            set({ error, loading: false });
        }
    }
}));

Conclusion

Zustand provides a flexible and powerful state management solution for React applications. By following these patterns and best practices, you can:

  1. Maintain clean and organized state management
  2. Optimize performance with selectors
  3. Handle state both inside and outside React components
  4. Leverage TypeScript for better type safety
  5. Implement advanced patterns for complex scenarios

Choose the approach that best fits your specific use case, and remember that Zustand's simplicity is one of its greatest strengths.

References