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
Feature Zustand Context API Boilerplate Minimal Extensive Performance Excellent Can cause re-renders DevTools ✅ Yes ❌ No Persistence ✅ Built-in ❌ Manual TypeScript ✅ Excellent ⚠️ Requires setup Bundle Size ~1KB 0KB (native) Learning Curve Low Medium
Store Architecture
The application uses multiple stores, each handling a specific domain:
Auth Store
UI Store
Posts Store
Notification Store
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
})
}
));
Manages global UI state (theme, sidebar, modals). interface UIState {
theme : 'light' | 'dark' | 'system' ;
sidebarOpen : boolean ;
mobileMenuOpen : boolean ;
globalLoading : boolean ;
activeModal : string | null ;
searchOpen : boolean ;
setTheme : ( theme : 'light' | 'dark' | 'system' ) => void ;
toggleSidebar : () => void ;
setSidebarOpen : ( open : boolean ) => void ;
toggleMobileMenu : () => void ;
setGlobalLoading : ( loading : boolean ) => void ;
openModal : ( modalId : string ) => void ;
closeModal : () => void ;
toggleSearch : () => void ;
}
export const useUIStore = create < UIState >()( persist (
( set ) => ({
theme: 'system' ,
sidebarOpen: true ,
mobileMenuOpen: false ,
globalLoading: false ,
activeModal: null ,
searchOpen: false ,
setTheme : ( theme ) => set ({ theme }),
toggleSidebar : () => set (( state ) => ({
sidebarOpen: ! state . sidebarOpen
})),
setSidebarOpen : ( open ) => set ({ sidebarOpen: open }),
toggleMobileMenu : () => set (( state ) => ({
mobileMenuOpen: ! state . mobileMenuOpen
})),
setGlobalLoading : ( loading ) => set ({ globalLoading: loading }),
openModal : ( modalId ) => set ({ activeModal: modalId }),
closeModal : () => set ({ activeModal: null }),
toggleSearch : () => set (( state ) => ({
searchOpen: ! state . searchOpen
}))
}),
{
name: 'ui-storage' ,
partialize : ( state ) => ({
theme: state . theme ,
sidebarOpen: state . sidebarOpen
})
}
));
Manages posts data and filters. interface PostsState {
posts : Post [];
selectedPost : Post | null ;
isLoading : boolean ;
error : string | null ;
filters : {
status : 'all' | 'published' | 'draft' | 'pending' ;
category : string | null ;
search : string ;
};
fetchPosts : () => Promise < void >;
setSelectedPost : ( post : Post | null ) => void ;
updatePost : ( id : string , status : string ) => Promise < void >;
deletePost : ( id : string ) => Promise < void >;
setFilters : ( filters : Partial < PostsState [ 'filters' ]>) => void ;
clearFilters : () => void ;
getFilteredPosts : () => Post [];
getPendingCount : () => number ;
}
export const usePostsStore = create < PostsState >()(( set , get ) => ({
posts: [],
selectedPost: null ,
isLoading: false ,
error: null ,
filters: {
status: 'all' ,
category: null ,
search: ''
},
fetchPosts : async () => {
set ({ isLoading: true });
try {
const posts = await postsService . getAllPosts ();
set ({ posts , isLoading: false });
} catch ( error ) {
set ({ error: error . message , isLoading: false });
}
},
getFilteredPosts : () => {
const { posts , filters } = get ();
return posts . filter ( post => {
if ( filters . status !== 'all' && post . status !== filters . status ) {
return false ;
}
if ( filters . category && post . category !== filters . category ) {
return false ;
}
if ( filters . search && ! post . title . toLowerCase (). includes (
filters . search . toLowerCase ()
)) {
return false ;
}
return true ;
});
},
getPendingCount : () => {
return get (). posts . filter ( p => p . status === 'pending' ). length ;
},
// ... other actions
}));
Manages toast notifications. interface Notification {
id : string ;
type : 'success' | 'error' | 'warning' | 'info' ;
title : string ;
message : string ;
duration ?: number ;
}
interface NotificationState {
notifications : Notification [];
addNotification : ( notification : Omit < Notification , 'id' >) => void ;
removeNotification : ( id : string ) => void ;
clearAll : () => void ;
}
export const useNotificationStore = create < NotificationState >()(
( set ) => ({
notifications: [],
addNotification : ( notification ) => {
const id = Math . random (). toString ( 36 ). substr ( 2 , 9 );
const newNotification = { ... notification , id };
set (( state ) => ({
notifications: [ ... state . notifications , newNotification ]
}));
// Auto-remove after duration
const duration = notification . duration || 5000 ;
setTimeout (() => {
set (( state ) => ({
notifications: state . notifications . filter ( n => n . id !== id )
}));
}, duration );
},
removeNotification : ( id ) => set (( state ) => ({
notifications: state . notifications . filter ( n => n . id !== id )
})),
clearAll : () => set ({ notifications: [] })
})
);
Usage Patterns
Pattern 1: Selector-Based (Recommended)
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
Login Component
Protected Route
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.
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
3. Use Shallow Comparison
When selecting multiple values, use shallow comparison: import { shallow } from 'zustand/shallow' ;
const { user , isAuthenticated } = useAuthStore (
( state ) => ({
user: state . user ,
isAuthenticated: state . isAuthenticated
}),
shallow
);
4. Async Actions with Try-Catch
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 });
}
}
5. Computed Values in Store
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 ();
});
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