Skip to main content

Overview

The Blog Marketing Platform uses Zustand for state management. Zustand is a lightweight, simple, and powerful state management library that provides a minimal API with excellent TypeScript support.
Zustand stores are referenced in the codebase documentation (ZUSTAND_GUIDE.md) but implementation may vary. This guide covers the recommended patterns.

Why Zustand?

Minimal Boilerplate

Less code than Redux or Context API

Excellent Performance

No unnecessary re-renders, selector-based updates

TypeScript First

Full type safety out of the box

DevTools Support

Redux DevTools integration included

Comparison with Context API

FeatureZustandContext API
BoilerplateMinimalExtensive
PerformanceExcellentCan cause re-renders
DevTools✅ Yes❌ No
Persistence✅ Built-in❌ Manual
TypeScript✅ Excellent⚠️ Requires setup
Bundle Size~1KB0KB (native)
Learning CurveLowMedium

Store Architecture

The application uses multiple stores, each handling a specific domain:
Manages user authentication and session state.
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface AuthState {
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;
  isLoading: boolean;
  error: string | null;
  login: (credentials: LoginData) => Promise<void>;
  logout: () => void;
  register: (data: RegisterData) => Promise<void>;
  checkAuth: () => void;
  setUser: (user: User) => void;
  clearError: () => void;
}

export const useAuthStore = create<AuthState>()(persist(
  (set, get) => ({
    user: null,
    token: null,
    isAuthenticated: false,
    isLoading: false,
    error: null,
    
    login: async (credentials) => {
      set({ isLoading: true, error: null });
      try {
        const { user, token } = await authService.login(credentials);
        set({ user, token, isAuthenticated: true, isLoading: false });
      } catch (error) {
        set({ error: error.message, isLoading: false });
      }
    },
    
    logout: () => {
      authService.logout();
      set({ user: null, token: null, isAuthenticated: false });
    },
    
    // ... other actions
  }),
  {
    name: 'auth-storage',
    partialize: (state) => ({
      user: state.user,
      token: state.token,
      isAuthenticated: state.isAuthenticated
    })
  }
));

Usage Patterns

Use selectors to subscribe only to specific state:
import { useAuthStore } from '@/stores/authStore';

function UserProfile() {
  // ✅ GOOD - Only re-renders when user changes
  const user = useAuthStore((state) => state.user);
  const logout = useAuthStore((state) => state.logout);
  
  return (
    <div>
      <h1>{user?.name}</h1>
      <button onClick={logout}>Logout</button>
    </div>
  );
}
Avoid destructuring the entire store - it will cause re-renders on any state change.
// ❌ BAD - Re-renders on ANY store change
const { user, token, isLoading, error, login, logout } = useAuthStore();

// ✅ GOOD - Only subscribes to specific values
const user = useAuthStore((state) => state.user);
const login = useAuthStore((state) => state.login);

Pattern 2: Multiple Selectors

function Dashboard() {
  const user = useAuthStore((state) => state.user);
  const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
  const posts = usePostsStore((state) => state.posts);
  const isLoading = usePostsStore((state) => state.isLoading);
  
  // Component logic
}

Pattern 3: Computed Selectors

function PostsList() {
  // Computed value - filters posts client-side
  const pendingPosts = usePostsStore((state) => 
    state.posts.filter(p => p.status === 'pending')
  );
  
  return (
    <div>
      {pendingPosts.map(post => <PostCard key={post.id} post={post} />)}
    </div>
  );
}

Pattern 4: Actions Outside Components

Access store actions in utility functions:
// utils/auth.ts
import { useAuthStore } from '@/stores/authStore';

export function checkUserPermission(permission: string): boolean {
  // Use getState() to access store outside React
  const user = useAuthStore.getState().user;
  return user?.permissions.includes(permission) || false;
}

export async function refreshUserData() {
  const checkAuth = useAuthStore.getState().checkAuth;
  await checkAuth();
}

Common Patterns

Login Flow

import { useAuthStore } from '@/stores/authStore';
import { useNotificationStore } from '@/stores/notificationStore';

function LoginForm() {
  const login = useAuthStore((state) => state.login);
  const isLoading = useAuthStore((state) => state.isLoading);
  const addNotification = useNotificationStore(
    (state) => state.addNotification
  );
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    try {
      await login({ email, password });
      addNotification({
        type: 'success',
        title: 'Welcome!',
        message: 'You have successfully logged in'
      });
      navigate('/dashboard');
    } catch (error) {
      addNotification({
        type: 'error',
        title: 'Login Failed',
        message: error.message
      });
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      {/* form fields */}
      <button disabled={isLoading}>
        {isLoading ? 'Loading...' : 'Login'}
      </button>
    </form>
  );
}

Theme Switching

import { useUIStore } from '@/stores/uiStore';
import { Sun, Moon, Monitor } from 'lucide-react';

function ThemeSwitcher() {
  const theme = useUIStore((state) => state.theme);
  const setTheme = useUIStore((state) => state.setTheme);
  
  return (
    <div className="flex space-x-2">
      <button onClick={() => setTheme('light')}>
        <Sun className={theme === 'light' ? 'text-yellow-500' : ''} />
      </button>
      <button onClick={() => setTheme('dark')}>
        <Moon className={theme === 'dark' ? 'text-blue-500' : ''} />
      </button>
      <button onClick={() => setTheme('system')}>
        <Monitor className={theme === 'system' ? 'text-gray-500' : ''} />
      </button>
    </div>
  );
}

Posts Management

import { usePostsStore } from '@/stores/postsStore';
import { useEffect } from 'react';

function PostsDashboard() {
  const fetchPosts = usePostsStore((state) => state.fetchPosts);
  const isLoading = usePostsStore((state) => state.isLoading);
  const getFilteredPosts = usePostsStore((state) => state.getFilteredPosts);
  const setFilters = usePostsStore((state) => state.setFilters);
  
  useEffect(() => {
    fetchPosts();
  }, [fetchPosts]);
  
  const posts = getFilteredPosts();
  
  return (
    <div>
      <input
        type="text"
        placeholder="Search..."
        onChange={(e) => setFilters({ search: e.target.value })}
      />
      
      <select onChange={(e) => setFilters({ status: e.target.value })}>
        <option value="all">All</option>
        <option value="published">Published</option>
        <option value="draft">Drafts</option>
        <option value="pending">Pending</option>
      </select>
      
      {isLoading ? (
        <div>Loading...</div>
      ) : (
        <div>
          {posts.map(post => <PostCard key={post.id} post={post} />)}
        </div>
      )}
    </div>
  );
}

Persistence

Stores can persist state to localStorage:
import { persist } from 'zustand/middleware';

export const useAuthStore = create<AuthState>()(persist(
  (set, get) => ({
    // store definition
  }),
  {
    name: 'auth-storage', // localStorage key
    partialize: (state) => ({
      // Only persist these fields
      user: state.user,
      token: state.token,
      isAuthenticated: state.isAuthenticated
    })
  }
));
Persisted state is automatically restored when the app loads.

DevTools Integration

Zustand supports Redux DevTools:
import { devtools } from 'zustand/middleware';

export const usePostsStore = create<PostsState>()(devtools(
  (set, get) => ({
    // store definition
  }),
  { name: 'PostsStore' }
));
Install the Redux DevTools extension to inspect state changes.

Best Practices

Always use selectors to avoid unnecessary re-renders:
// ✅ GOOD
const user = useAuthStore((state) => state.user);

// ❌ BAD
const { user, token, isLoading } = useAuthStore();
Create separate stores for different domains:
  • authStore - Authentication
  • uiStore - UI state
  • postsStore - Posts data
  • notificationStore - Notifications
When selecting multiple values, use shallow comparison:
import { shallow } from 'zustand/shallow';

const { user, isAuthenticated } = useAuthStore(
  (state) => ({ 
    user: state.user, 
    isAuthenticated: state.isAuthenticated 
  }),
  shallow
);
Always handle errors in async actions:
login: async (credentials) => {
  set({ isLoading: true, error: null });
  try {
    const { user, token } = await authService.login(credentials);
    set({ user, token, isAuthenticated: true, isLoading: false });
  } catch (error) {
    set({ error: error.message, isLoading: false });
  }
}
Add computed methods to stores for common queries:
getFilteredPosts: () => {
  const { posts, filters } = get();
  return posts.filter(/* filter logic */);
},
getPendingCount: () => {
  return get().posts.filter(p => p.status === 'pending').length;
}

Testing Stores

import { renderHook, act } from '@testing-library/react';
import { useAuthStore } from '@/stores/authStore';

test('login sets user and token', async () => {
  const { result } = renderHook(() => useAuthStore());
  
  await act(async () => {
    await result.current.login({
      email: '[email protected]',
      password: 'password'
    });
  });
  
  expect(result.current.isAuthenticated).toBe(true);
  expect(result.current.user).toBeTruthy();
});

Performance Tips

Selector Optimization

Use specific selectors to minimize re-renders

Shallow Comparison

Use shallow when selecting multiple values

Memoization

Memoize computed selectors if expensive

Split Stores

Keep stores focused and small

Resources

Zustand Docs

Official Zustand documentation

Middleware

Learn about Zustand middleware

Persist

Persist middleware documentation

DevTools

Using Redux DevTools with Zustand

Next Steps

Architecture

Understanding the overall architecture

Services

Learn about API service layer

Components

Explore component patterns

Best Practices

Development best practices

Build docs developers (and LLMs) love