Advanced State Management Patterns in Single-Page Applications
In this tutorial, you will learn about Advanced State Management Patterns in Single. We cover key concepts, practical examples, and best practices to help you master this topic.
Advanced state management in SPAs covers observable stores, selectors, middleware for side effects, devtools integration, and performance patterns for complex application state at scale.
What You'll Learn
By the end of this tutorial, you will understand advanced state management patterns including observable stores with selectors, middleware pipelines for side effects, devtools integration, and performance optimization strategies for large-scale SPA state.
Why It Matters
As SPAs grow beyond simple CRUD apps, state management becomes the primary bottleneck. Without advanced patterns, you get unmanageable stores, performance degradation from unnecessary re-renders, and side effects scattered across components. Professional-grade SPAs need maintainable, performant, and debuggable state.
Real-World Use
A SaaS dashboard with real-time data streams from Websocket connections, user preferences, cached API responses, and UI state needs a store architecture that handles optimistic updates, request deduplication, background revalidation, and time-travel debugging without slowing down.
State Management Architecture
┌──────────────────────────────────────────────────────────┐
│ Advanced State Architecture │
├──────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐ │
│ │ UI │ │ Side │ │ API │ │ Cache │ │
│ │ State │ │ Effects │ │ State │ │ Layer │ │
│ │ (local) │ │ Middle- │ │ (server)│ │ (SWR/ │ │
│ │ │ │ ware │ │ │ │ React │ │
│ │ │ │ │ │ │ │ Query) │ │
│ └────┬────┘ └────┬─────┘ └────┬────┘ └────┬─────┘ │
│ └────────────┼─────────────┼────────────┘ │
│ └─────────────┘ │
│ │ │
│ ┌─────────▼──────────┐ │
│ │ Store (Zustand │ │
│ │ Redux, Pinia) │ │
│ └────────────────────┘ │
└──────────────────────────────────────────────────────────┘
Think of advanced state management like a city's traffic control system. Local state is stop signs at a single intersection. A basic store is a traffic light. Selectors are Express lanes that bypass traffic. Middleware is the control room that monitors and coordinates everything. Devtools are the traffic cameras that let you replay any moment.
Observable Stores with Selectors
import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';
// Store with selectors for fine-grained subscriptions
const useAppStore = create(
subscribeWithSelector((set, get) => ({
users: [],
posts: [],
comments: {},
selectedUserId: null,
loading: false,
error: null,
fetchUsers: async () => {
set({ loading: true, error: null });
try {
const response = await fetch('/api/users');
const users = await response.json();
set({ users, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
selectUser: (userId) => {
set({ selectedUserId: userId });
},
getSelectedUser: () => {
const { users, selectedUserId } = get();
return users.find(u => u.id === selectedUserId) || null;
}
}))
);
// Component with selector — only re-renders when users change
function UserList() {
const users = useAppStore(state => state.users);
const selectUser = useAppStore(state => state.selectUser);
return (
<ul>
{users.map(user => (
<li key={user.id} onClick={() => selectUser(user.id)}>
{user.name}
</li>
))}
</ul>
);
}
// Subscribe to specific slice changes
const unsubscribe = useAppStore.subscribe(
(state) => state.selectedUserId,
(selectedUserId, previousSelectedUserId) => {
console.log(`Selection changed: ${previousSelectedUserId} -> ${selectedUserId}`);
// Log analytics, fetch user details, etc.
}
);
Middleware Pipeline for Side Effects
import { create } from 'zustand';
import { devtools, persist, redux } from 'zustand/middleware';
// Middleware pipeline: logging -> persistence -> devtools
const useMiddlewareStore = create(
devtools(
persist(
(set) => ({
cart: [],
addToCart: (product) =>
set((state) => ({
cart: [...state.cart, product]
})),
removeFromCart: (productId) =>
set((state) => ({
cart: state.cart.filter(p => p.id !== productId)
})),
clearCart: () => set({ cart: [] })
}),
{
name: 'cart-storage',
partialize: (state) => ({ cart: state.cart }),
}
),
{ name: 'CartStore' }
)
);
// Custom logging middleware
const logMiddleware = (config) => (set, get, api) =>
config(
(...args) => {
console.log('Action before:', args);
const result = set(...args);
console.log('State after:', get());
return result;
},
get,
api
);
const useLoggedStore = create(
logMiddleware((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
);
Server State Management with React Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Server state is separate from UI state
function Dashboard() {
const queryClient = useQueryClient();
// Fetch with caching, deduplication, background refetch
const { data: users, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json()),
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 30 * 60 * 1000, // 30 minutes
refetchOnWindowFocus: true,
});
// Mutation with optimistic updates
const updateUserMutation = useMutation({
mutationFn: (updatedUser) =>
fetch(`/api/users/${updatedUser.id}`, {
method: 'PUT',
body: JSON.stringify(updatedUser),
headers: { 'Content-Type': 'application/json' }
}).then(r => r.json()),
onMutate: async (updatedUser) => {
await queryClient.cancelQueries(['users']);
const previous = queryClient.getQueryData(['users']);
queryClient.setQueryData(['users'], (old) =>
old.map(u => u.id === updatedUser.id ? { ...u, ...updatedUser } : u)
);
return { previous };
},
onError: (err, updatedUser, context) => {
queryClient.setQueryData(['users'], context.previous);
},
onSettled: () => {
queryClient.invalidateQueries(['users']);
}
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Common Mistakes
- Mixing server state with UI state in the same store. Server state (API data) has different requirements than UI state (loading, modals). Use separate stores or dedicated libraries like React Query for server state.
- Creating too many subscriptions. Every
useStore(state => state.x)creates a subscription. Excessive subscriptions cause unnecessary re-renders. Batch related state into derived slices. - Mutating state directly in selectors. Selectors should be pure functions. Creating new objects inside selectors breaks referential equality and causes infinite re-renders.
- Ignoring middleware order. The order of middleware in the pipeline matters. Devtools should be outermost, persist middleware handles Serialization, and custom middleware goes between them.
- Not cleaning up subscriptions. Subscriptions created in
useEffectmust be cleaned up. Unsubscribing prevents memory leaks in long-lived SPAs.
Practice Questions
- What is the difference between server state and UI state, and why should they be managed separately?
- How do selectors improve performance in observable stores?
- What is the purpose of middleware in a state management pipeline?
- How does optimistic updating work with React Query mutations?
- Why should derived state be computed rather than stored?
Challenge: Build a real-time dashboard that uses Zustand for UI state (theme, sidebar open/close, selected filters), React Query for server state (user list, analytics data fetched from API), and a middleware pipeline that logs all Zustand actions to a remote logging service while persisting UI preferences to localStorage.
FAQ
Mini Project
Build a project management dashboard with Zustand for UI state (collapsed sections, current view, filters), React Query for server state (tasks, projects, team members from a mock API), and persist middleware that saves user preferences. Implement optimistic updates for task completion and measure re-renders with React DevTools.
What's Next
You understand advanced state management. Now explore SPA SEO strategies to make your stateful applications discoverable by search engines.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro