Skip to content

Advanced State Management Patterns in Single-Page Applications

DodaTech Updated 2026-06-28 6 min read

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

  1. 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.
  2. Creating too many subscriptions. Every useStore(state => state.x) creates a subscription. Excessive subscriptions cause unnecessary re-renders. Batch related state into derived slices.
  3. Mutating state directly in selectors. Selectors should be pure functions. Creating new objects inside selectors breaks referential equality and causes infinite re-renders.
  4. 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.
  5. Not cleaning up subscriptions. Subscriptions created in useEffect must be cleaned up. Unsubscribing prevents memory leaks in long-lived SPAs.

Practice Questions

  1. What is the difference between server state and UI state, and why should they be managed separately?
  2. How do selectors improve performance in observable stores?
  3. What is the purpose of middleware in a state management pipeline?
  4. How does optimistic updating work with React Query mutations?
  5. 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

When should I use Zustand vs React Query?

Use Zustand for client state (UI toggles, form inputs, theme). Use React Query for server state (API data, caching, background refetching). They complement each other.

What is the difference between subscribeWithSelector and vanilla subscribe?

subscribeWithSelector lets you subscribe to a specific slice of state with a selector function, so the callback only fires when that slice changes. Vanilla subscribe fires on any state change.

How do I prevent Zustand from persisting sensitive data?

Use the partialize option in persist middleware to whitelist only non-sensitive fields. Never persist auth tokens or user credentials.

Can I use Redux DevTools with Zustand?

Yes. Zustand ships with a devtools middleware that connects to the Redux DevTools browser extension, giving you time-travel debugging and state inspection.

What causes infinite re-renders with Zustand selectors?

Creating new objects or arrays inside selectors breaks referential equality. Use shallow comparison or ensure selectors return primitive values or stable references.

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