The State Management Landscape: Choosing Your Weapon
State management in React is like choosing transportation for a journey. Walking (local state) works for short distances, a bike (Context API) for medium trips, a car (Zustand) for flexibility, or a train (Redux) for long, complex journeys with many passengers (data).
| Solution | Best For | Learning Curve | Bundle Size | DevTools |
|---|---|---|---|---|
| useState | Component-level state | ⭐ Easy | 0KB (built-in) | React DevTools |
| Context API | Theme, Auth, Settings | ⭐⭐ Moderate | 0KB (built-in) | React DevTools |
| Zustand | App-wide state | ⭐⭐ Moderate | ~8KB | Zustand DevTools |
| React Query | Server state, API cache | ⭐⭐⭐ Advanced | ~40KB | React Query DevTools |
| Redux Toolkit | Complex apps, time-travel | ⭐⭐⭐⭐ Complex | ~45KB | Redux DevTools |
Context API: The Broadcasting Tower
Context is like a radio broadcasting tower. Any component can tune in (useContext) to receive the signal (state), avoiding the telephone game of passing props through every component.
UserProvider
CartProvider] end subgraph "Component Tree" App[App] Header[Header] Main[Main] Footer[Footer] Nav[Navigation] Profile[Profile] Cart[CartIcon] Products[ProductList] Settings[Settings] end Provider -.->|broadcasts| Header Provider -.->|broadcasts| Profile Provider -.->|broadcasts| Cart Provider -.->|broadcasts| Settings Provider -.->|broadcasts| Products App --> Header App --> Main App --> Footer Header --> Nav Nav --> Profile Nav --> Cart Main --> Products Footer --> Settings style Provider fill:#3498db,stroke:#333,stroke-width:3px style Profile fill:#2ecc71 style Cart fill:#2ecc71 style Settings fill:#2ecc71 style Products fill:#2ecc71
Pattern 1: Multi-Provider Pattern
Organizing Multiple Contexts
// providers/AppProviders.js
import { ThemeProvider } from './ThemeContext';
import { AuthProvider } from './AuthContext';
import { WordPressProvider } from './WordPressContext';
export const AppProviders = ({ children }) => {
return (
<ThemeProvider>
<AuthProvider>
<WordPressProvider>
{children}
</WordPressProvider>
</AuthProvider>
</ThemeProvider>
);
};
// App.js
import { AppProviders } from './providers/AppProviders';
function App() {
return (
<AppProviders>
<Router>
<YourAppContent />
</Router>
</AppProviders>
);
}
Pattern 2: WordPress Data Context
Managing WordPress Content with Context
// contexts/WordPressContext.js
import React, { createContext, useContext, useReducer, useEffect } from 'react';
import { WordPressAPI } from '../services/api';
const WordPressContext = createContext();
// Action types
const ACTIONS = {
FETCH_POSTS_START: 'FETCH_POSTS_START',
FETCH_POSTS_SUCCESS: 'FETCH_POSTS_SUCCESS',
FETCH_POSTS_ERROR: 'FETCH_POSTS_ERROR',
SET_CURRENT_POST: 'SET_CURRENT_POST',
UPDATE_CACHE: 'UPDATE_CACHE'
};
// Reducer
const wordpressReducer = (state, action) => {
switch (action.type) {
case ACTIONS.FETCH_POSTS_START:
return { ...state, loading: true, error: null };
case ACTIONS.FETCH_POSTS_SUCCESS:
return {
...state,
posts: action.payload,
loading: false,
lastFetch: Date.now()
};
case ACTIONS.FETCH_POSTS_ERROR:
return { ...state, loading: false, error: action.payload };
case ACTIONS.SET_CURRENT_POST:
return { ...state, currentPost: action.payload };
default:
return state;
}
};
// Provider component
export const WordPressProvider = ({ children }) => {
const [state, dispatch] = useReducer(wordpressReducer, {
posts: [],
currentPost: null,
loading: false,
error: null,
lastFetch: null
});
// Fetch posts with caching
const fetchPosts = async (force = false) => {
// Check cache validity (5 minutes)
const cacheValid = state.lastFetch &&
(Date.now() - state.lastFetch) < 5 * 60 * 1000;
if (!force && cacheValid) return;
dispatch({ type: ACTIONS.FETCH_POSTS_START });
try {
const posts = await WordPressAPI.getPosts();
dispatch({ type: ACTIONS.FETCH_POSTS_SUCCESS, payload: posts });
} catch (error) {
dispatch({ type: ACTIONS.FETCH_POSTS_ERROR, payload: error.message });
}
};
const value = {
...state,
fetchPosts,
setCurrentPost: (post) =>
dispatch({ type: ACTIONS.SET_CURRENT_POST, payload: post })
};
return (
<WordPressContext.Provider value={value}>
{children}
</WordPressContext.Provider>
);
};
// Custom hook
export const useWordPress = () => {
const context = useContext(WordPressContext);
if (!context) {
throw new Error('useWordPress must be used within WordPressProvider');
}
return context;
};
Redux with WordPress: The Command Center
Redux is like NASA's mission control - every action is logged, every state change is tracked, and you can literally time-travel through your application's history. Perfect for complex WordPress applications with multiple data sources.
Redux Toolkit with WordPress
Modern Redux Setup for WordPress
// store/slices/wordpressSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { WordPressAPI } from '../../services/api';
// Async thunks for WordPress API
export const fetchPosts = createAsyncThunk(
'wordpress/fetchPosts',
async ({ page = 1, perPage = 10 }, { rejectWithValue }) => {
try {
const response = await WordPressAPI.getPosts({ page, perPage });
return response;
} catch (error) {
return rejectWithValue(error.message);
}
}
);
export const fetchPostById = createAsyncThunk(
'wordpress/fetchPostById',
async (id) => {
const response = await WordPressAPI.getPost(id);
return response;
}
);
export const createComment = createAsyncThunk(
'wordpress/createComment',
async ({ postId, content, authorName, authorEmail }) => {
const response = await WordPressAPI.createComment({
post: postId,
content,
author_name: authorName,
author_email: authorEmail
});
return response;
}
);
// Slice
const wordpressSlice = createSlice({
name: 'wordpress',
initialState: {
posts: {
items: [],
currentPage: 1,
totalPages: 0,
total: 0,
loading: false,
error: null
},
currentPost: {
data: null,
comments: [],
loading: false,
error: null
},
categories: [],
tags: []
},
reducers: {
clearCurrentPost: (state) => {
state.currentPost = { data: null, comments: [], loading: false, error: null };
},
updatePostInCache: (state, action) => {
const index = state.posts.items.findIndex(p => p.id === action.payload.id);
if (index !== -1) {
state.posts.items[index] = action.payload;
}
}
},
extraReducers: (builder) => {
builder
// Fetch posts
.addCase(fetchPosts.pending, (state) => {
state.posts.loading = true;
state.posts.error = null;
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.posts.loading = false;
state.posts.items = action.payload.posts;
state.posts.totalPages = action.payload.totalPages;
state.posts.total = action.payload.total;
})
.addCase(fetchPosts.rejected, (state, action) => {
state.posts.loading = false;
state.posts.error = action.payload;
})
// Fetch single post
.addCase(fetchPostById.pending, (state) => {
state.currentPost.loading = true;
})
.addCase(fetchPostById.fulfilled, (state, action) => {
state.currentPost.loading = false;
state.currentPost.data = action.payload;
})
// Create comment
.addCase(createComment.fulfilled, (state, action) => {
state.currentPost.comments.push(action.payload);
});
}
});
export const { clearCurrentPost, updatePostInCache } = wordpressSlice.actions;
export default wordpressSlice.reducer;
// Selectors
export const selectAllPosts = (state) => state.wordpress.posts.items;
export const selectCurrentPost = (state) => state.wordpress.currentPost.data;
export const selectPostsLoading = (state) => state.wordpress.posts.loading;
Zustand: The Lightweight Champion
Zustand is like a smart filing cabinet - simple to use but surprisingly powerful. It combines the simplicity of useState with the power of global state management, perfect for WordPress apps that need more than Context but less than Redux.
posts, user, theme] end subgraph "Components" A[BlogList] B[UserProfile] C[ThemeToggle] D[Comments] end Store -.->|subscribe| A Store -.->|subscribe| B Store -.->|subscribe| C Store -.->|subscribe| D A -->|update| Store B -->|update| Store C -->|update| Store D -->|update| Store style Store fill:#9b59b6,stroke:#333,stroke-width:3px
Zustand WordPress Store
Complete Zustand Setup for WordPress
// stores/useWordPressStore.js
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { WordPressAPI } from '../services/api';
const useWordPressStore = create(
devtools(
persist(
(set, get) => ({
// State
posts: [],
currentPost: null,
categories: [],
searchResults: [],
filters: {
category: null,
tag: null,
search: ''
},
ui: {
loading: false,
error: null,
theme: 'light'
},
// Actions
fetchPosts: async (options = {}) => {
set({ ui: { ...get().ui, loading: true, error: null } });
try {
const posts = await WordPressAPI.getPosts(options);
set({
posts,
ui: { ...get().ui, loading: false }
});
return posts;
} catch (error) {
set({
ui: { ...get().ui, loading: false, error: error.message }
});
throw error;
}
},
fetchPostBySlug: async (slug) => {
// Check if already in cache
const cached = get().posts.find(p => p.slug === slug);
if (cached && !isStale(cached)) {
set({ currentPost: cached });
return cached;
}
set({ ui: { ...get().ui, loading: true } });
try {
const post = await WordPressAPI.getPostBySlug(slug);
set({
currentPost: post,
ui: { ...get().ui, loading: false }
});
// Update cache
const posts = get().posts;
const index = posts.findIndex(p => p.id === post.id);
if (index !== -1) {
posts[index] = post;
set({ posts: [...posts] });
}
return post;
} catch (error) {
set({
ui: { ...get().ui, loading: false, error: error.message }
});
throw error;
}
},
searchPosts: async (query) => {
set({
filters: { ...get().filters, search: query },
ui: { ...get().ui, loading: true }
});
try {
const results = await WordPressAPI.searchPosts(query);
set({
searchResults: results,
ui: { ...get().ui, loading: false }
});
return results;
} catch (error) {
set({
ui: { ...get().ui, loading: false, error: error.message }
});
throw error;
}
},
toggleTheme: () => {
const newTheme = get().ui.theme === 'light' ? 'dark' : 'light';
set({ ui: { ...get().ui, theme: newTheme } });
document.documentElement.classList.toggle('dark');
},
clearError: () => {
set({ ui: { ...get().ui, error: null } });
},
// Computed values
get filteredPosts() {
const { posts, filters } = get();
let filtered = posts;
if (filters.category) {
filtered = filtered.filter(p =>
p.categories.includes(filters.category)
);
}
if (filters.tag) {
filtered = filtered.filter(p =>
p.tags.includes(filters.tag)
);
}
if (filters.search) {
const query = filters.search.toLowerCase();
filtered = filtered.filter(p =>
p.title.rendered.toLowerCase().includes(query) ||
p.excerpt.rendered.toLowerCase().includes(query)
);
}
return filtered;
}
}),
{
name: 'wordpress-storage',
partialize: (state) => ({
posts: state.posts,
categories: state.categories,
ui: { theme: state.ui.theme }
})
}
)
)
);
// Utility function
function isStale(post, maxAge = 5 * 60 * 1000) {
return Date.now() - new Date(post.fetchedAt).getTime() > maxAge;
}
// Usage in components
export default useWordPressStore;
// Component example
function BlogList() {
const { posts, fetchPosts, ui } = useWordPressStore();
useEffect(() => {
if (posts.length === 0) {
fetchPosts();
}
}, []);
if (ui.loading) return ;
if (ui.error) return ;
return (
<div>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
React Query: The Server State Specialist
React Query (TanStack Query) is like having a smart assistant who manages all your server communications. It knows when to fetch, when to use cache, when to refetch, and handles all the loading states automatically.
React Query with WordPress
Advanced React Query Setup
// hooks/useWordPressQuery.js
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { WordPressAPI } from '../services/api';
// Query keys factory
const queryKeys = {
all: ['wordpress'],
posts: () => [...queryKeys.all, 'posts'],
post: (id) => [...queryKeys.posts(), id],
postBySlug: (slug) => [...queryKeys.posts(), 'slug', slug],
comments: (postId) => [...queryKeys.all, 'comments', postId],
categories: () => [...queryKeys.all, 'categories'],
search: (query) => [...queryKeys.all, 'search', query]
};
// Hooks
export function usePosts(options = {}) {
return useQuery({
queryKey: queryKeys.posts(),
queryFn: () => WordPressAPI.getPosts(options),
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false,
refetchOnMount: 'always'
});
}
export function usePost(slug) {
return useQuery({
queryKey: queryKeys.postBySlug(slug),
queryFn: () => WordPressAPI.getPostBySlug(slug),
staleTime: 10 * 60 * 1000,
enabled: !!slug // Only run if slug exists
});
}
export function useInfinitePosts() {
return useInfiniteQuery({
queryKey: queryKeys.posts(),
queryFn: ({ pageParam = 1 }) =>
WordPressAPI.getPosts({ page: pageParam, perPage: 10 }),
getNextPageParam: (lastPage, pages) => {
return lastPage.hasMore ? pages.length + 1 : undefined;
},
staleTime: 5 * 60 * 1000
});
}
export function useCreateComment() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (commentData) => WordPressAPI.createComment(commentData),
onSuccess: (newComment, variables) => {
// Update cache optimistically
queryClient.setQueryData(
queryKeys.comments(variables.postId),
(old) => [...(old || []), newComment]
);
// Invalidate post to refetch with new comment count
queryClient.invalidateQueries(queryKeys.post(variables.postId));
},
onError: (error, variables, context) => {
// Rollback optimistic update on error
if (context?.previousComments) {
queryClient.setQueryData(
queryKeys.comments(variables.postId),
context.previousComments
);
}
}
});
}
// Prefetching for better UX
export function usePrefetchPost() {
const queryClient = useQueryClient();
return (slug) => {
queryClient.prefetchQuery({
queryKey: queryKeys.postBySlug(slug),
queryFn: () => WordPressAPI.getPostBySlug(slug),
staleTime: 10 * 60 * 1000
});
};
}
// Component usage example
function BlogPost({ slug }) {
const { data: post, isLoading, error } = usePost(slug);
const { mutate: createComment } = useCreateComment();
if (isLoading) return ;
if (error) return ;
return (
<article>
<h1>{post.title.rendered}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content.rendered }} />
<CommentForm
onSubmit={(data) => createComment({ ...data, postId: post.id })}
/>
</article>
);
}
// App.js setup
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 2,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
refetchOnWindowFocus: false,
staleTime: 5 * 60 * 1000
}
}
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
State Persistence Strategies
State persistence is like having a photographic memory for your app. Even when users close the browser, come back tomorrow, or lose internet connection, your app remembers exactly where they left off.
Fastest, Temporary] SessionStorage[Session Storage
Tab lifetime] LocalStorage[Local Storage
Permanent] IndexedDB[IndexedDB
Large data] Server[Server/Cloud
Cross-device] end Memory --> SessionStorage SessionStorage --> LocalStorage LocalStorage --> IndexedDB IndexedDB --> Server style Memory fill:#2ecc71 style SessionStorage fill:#3498db style LocalStorage fill:#9b59b6 style IndexedDB fill:#f39c12 style Server fill:#e74c3c
Comprehensive Persistence Solution
Multi-Layer Persistence Strategy
// utils/persistence.js
class PersistenceManager {
constructor() {
this.storage = this.detectStorage();
this.prefix = 'wp_app_';
}
detectStorage() {
// Check available storage options
if (typeof window !== 'undefined') {
if ('indexedDB' in window) {
return new IndexedDBAdapter();
}
if ('localStorage' in window) {
return new LocalStorageAdapter();
}
}
return new MemoryAdapter();
}
async save(key, data, options = {}) {
const fullKey = this.prefix + key;
const dataToSave = {
data,
timestamp: Date.now(),
version: options.version || 1,
ttl: options.ttl || null
};
try {
if (options.encrypt) {
dataToSave.data = await this.encrypt(data);
}
await this.storage.setItem(fullKey, JSON.stringify(dataToSave));
// Sync to server if enabled
if (options.sync) {
this.syncToServer(key, data);
}
} catch (error) {
console.error('Persistence error:', error);
// Fallback to memory
this.fallbackMemory[fullKey] = dataToSave;
}
}
async load(key, options = {}) {
const fullKey = this.prefix + key;
try {
const stored = await this.storage.getItem(fullKey);
if (!stored) return null;
const parsed = JSON.parse(stored);
// Check TTL
if (parsed.ttl && Date.now() - parsed.timestamp > parsed.ttl) {
await this.storage.removeItem(fullKey);
return null;
}
// Check version
if (options.version && parsed.version !== options.version) {
await this.migrate(key, parsed, options.version);
}
if (options.decrypt && parsed.encrypted) {
parsed.data = await this.decrypt(parsed.data);
}
return parsed.data;
} catch (error) {
console.error('Load error:', error);
return this.fallbackMemory[fullKey]?.data || null;
}
}
async clear(pattern) {
const keys = await this.storage.keys();
const toDelete = keys.filter(key =>
key.startsWith(this.prefix) &&
(!pattern || key.includes(pattern))
);
await Promise.all(toDelete.map(key =>
this.storage.removeItem(key)
));
}
// IndexedDB Adapter for large data
class IndexedDBAdapter {
constructor() {
this.dbName = 'WordPressAppDB';
this.storeName = 'state';
this.db = null;
}
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
}
};
});
}
async setItem(key, value) {
if (!this.db) await this.init();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.put(value, key);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async getItem(key) {
if (!this.db) await this.init();
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
}
// Custom hook for persisted state
export function usePersistedState(key, initialValue, options = {}) {
const [state, setState] = useState(initialValue);
const [isLoading, setIsLoading] = useState(true);
const persistence = useRef(new PersistenceManager());
// Load persisted state
useEffect(() => {
persistence.current.load(key, options).then(saved => {
if (saved !== null) {
setState(saved);
}
setIsLoading(false);
});
}, [key]);
// Save state changes
const setPersistedState = useCallback((value) => {
setState(value);
persistence.current.save(key, value, options);
}, [key, options]);
return [state, setPersistedState, isLoading];
}
// Usage example
function BlogReader() {
const [readPosts, setReadPosts] = usePersistedState('read_posts', [], {
ttl: 30 * 24 * 60 * 60 * 1000, // 30 days
sync: true
});
const [preferences, setPreferences] = usePersistedState('user_prefs', {
theme: 'light',
fontSize: 16,
notifications: true
}, {
encrypt: true,
version: 2
});
// Component logic...
}
- Session Storage: Form drafts, temporary UI state
- Local Storage: User preferences, read articles
- IndexedDB: Offline cache, large datasets
- Server: Cross-device sync, backups
Choosing the Right Solution
Selecting the right state management solution is like choosing the right vehicle for a journey. Here's your decision matrix:
Quick Decision Guide
- useState: Form inputs, toggles, simple counters
- Context API: Theme, authentication, user preferences
- Zustand: Shopping cart, user sessions, app-wide UI state
- React Query: Blog posts, comments, any WordPress data
- Redux: Complex e-commerce, real-time collaboration, undo/redo
Real-World Example: Complete Blog State Architecture
Let's combine everything into a production-ready WordPress blog application using the best of each solution:
Hybrid State Management Architecture
// Complete state architecture for a WordPress blog
// 1. React Query for server state (posts, comments)
// hooks/queries.js
export const useBlogPosts = () => useQuery({
queryKey: ['posts'],
queryFn: WordPressAPI.getPosts
});
// 2. Zustand for client state (UI, preferences)
// stores/uiStore.js
export const useUIStore = create(persist(
(set) => ({
theme: 'light',
sidebarOpen: false,
toggleTheme: () => set(state => ({
theme: state.theme === 'light' ? 'dark' : 'light'
})),
toggleSidebar: () => set(state => ({
sidebarOpen: !state.sidebarOpen
}))
}),
{ name: 'ui-preferences' }
));
// 3. Context for authentication
// contexts/AuthContext.js
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const login = async (credentials) => {
const user = await WordPressAPI.login(credentials);
setUser(user);
localStorage.setItem('wp_token', user.token);
};
return (
<AuthContext.Provider value={{ user, login }}>
{children}
</AuthContext.Provider>
);
};
// 4. Local state for forms
// components/CommentForm.js
function CommentForm({ postId }) {
const [comment, setComment] = useState('');
const mutation = useCreateComment();
const handleSubmit = (e) => {
e.preventDefault();
mutation.mutate({ postId, content: comment });
setComment('');
};
return (
<form onSubmit={handleSubmit}>
<textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
/>
<button type="submit">Post Comment</button>
</form>
);
}
// 5. Main App with all providers
function App() {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<BlogApp />
</AuthProvider>
</QueryClientProvider>
);
}