State Management Architecture Implementation

1. Redux Toolkit RTK Query

Concept API Description Use Case
Redux Toolkit NEW @reduxjs/toolkit Official opinionated Redux toolset with simplified API, built-in Immer, thunks Complex state logic, large applications, time-travel debugging
createSlice createSlice() Generate action creators and reducers automatically with less boilerplate Feature-based state management with auto-generated actions
RTK Query createApi() Data fetching and caching layer built on Redux, auto-generates hooks REST/GraphQL APIs with automatic cache invalidation
Immer Integration state.value++ Write mutable code that produces immutable updates automatically Simplified reducer logic, no spread operators needed
DevTools Extension Redux DevTools Time-travel debugging, action replay, state inspection Debugging complex state transitions, bug reproduction

Example: Redux Toolkit with RTK Query

// store/slices/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value++; }, // Immer allows mutation
    decrement: (state) => { state.value--; },
    incrementByAmount: (state, action) => { state.value += action.payload; }
  }
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;

// store/api/postsApi.js - RTK Query
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const postsApi = createApi({
  reducerPath: 'postsApi',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Post'],
  endpoints: (builder) => ({
    getPosts: builder.query({
      query: () => '/posts',
      providesTags: ['Post']
    }),
    addPost: builder.mutation({
      query: (post) => ({ url: '/posts', method: 'POST', body: post }),
      invalidatesTags: ['Post'] // Auto-refetch on mutation
    })
  })
});

export const { useGetPostsQuery, useAddPostMutation } = postsApi;

// Usage in component
function PostList() {
  const { data, isLoading, error } = useGetPostsQuery();
  const [addPost] = useAddPostMutation();
  
  if (isLoading) return <div>Loading...</div>;
  return <div>{data.map(post => <Post key={post.id} {...post} />)}</div>;
}
Key Benefits: 95% less boilerplate vs classic Redux, automatic cache invalidation, optimistic updates, request deduplication, polling support

2. Zustand Lightweight State Management

Feature Syntax Description Benefit
Minimal API create() Simple hook-based API without providers, actions, or reducers 3KB bundle, zero boilerplate, easy learning curve
No Provider Wrapper useStore() Direct store access without Context Provider hierarchy Cleaner component tree, no context re-render issues
Transient Updates subscribe() Subscribe to store changes without causing re-renders Performance optimization for animations, frequent updates
Selector Functions useStore(state => state.x) Fine-grained subscriptions to specific state slices Prevents unnecessary re-renders, optimized performance
Middleware Support persist, devtools Optional plugins for persistence, debugging, async actions Flexible enhancement without core complexity

Example: Zustand Store Implementation

import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';

// Simple store
const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 })
}));

// With middleware (persistence + devtools)
const useBearStore = create(
  devtools(
    persist(
      (set) => ({
        bears: 0,
        addBear: () => set((state) => ({ bears: state.bears + 1 })),
        removeAllBears: () => set({ bears: 0 })
      }),
      { name: 'bear-storage' } // localStorage key
    )
  )
);

// Usage - selector prevents re-render unless count changes
function Counter() {
  const count = useStore((state) => state.count);
  const increment = useStore((state) => state.increment);
  return <button onClick={increment}>{count}</button>;
}

// Transient updates (no re-render)
useStore.subscribe(
  (state) => state.count,
  (count) => console.log('Count changed:', count)
);

Zustand vs Redux

  • Bundle Size: Zustand 3KB vs Redux Toolkit 12KB
  • Boilerplate: Single create() call vs slice/store/provider setup
  • Performance: Selective subscriptions prevent unnecessary renders
  • Use Case: Small-to-medium apps, simple state, rapid prototyping

3. Context API React State

Concept API Description Best Practice
createContext React.createContext() Create context object for passing data through component tree Use for infrequently changing data (theme, auth, locale)
Context Provider <Context.Provider> Wrap components to provide context value to descendants Split contexts by update frequency to minimize re-renders
useContext Hook useContext(Context) Subscribe to context value in functional components Combine with useMemo to prevent unnecessary calculations
Re-render Issue Every consumer re-renders All context consumers re-render when provider value changes Use composition, memo, or external library for optimization
Prop Drilling Solution Context + Custom Hook Avoid passing props through multiple component layers Create custom useAuth(), useTheme() hooks for clean API

Example: Optimized Context Pattern

// contexts/AuthContext.js
import { createContext, useContext, useState, useMemo } from 'react';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [token, setToken] = useState(null);
  
  // Memoize value to prevent re-renders when provider re-renders
  const value = useMemo(() => ({
    user,
    token,
    login: (userData, authToken) => {
      setUser(userData);
      setToken(authToken);
    },
    logout: () => {
      setUser(null);
      setToken(null);
    }
  }), [user, token]);
  
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

// Custom hook with error handling
export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}

// Usage
function App() {
  return (
    <AuthProvider>
      <Dashboard />
    </AuthProvider>
  );
}

function Dashboard() {
  const { user, logout } = useAuth();
  return <button onClick={logout}>{user.name}</button>;
}
Warning: Context API is not optimized for frequent updates. For high-frequency state changes (form inputs, animations), use local state or specialized libraries like Zustand/Jotai.

4. MobX Observable State Trees

Concept Decorator/API Description Advantage
Observable State makeObservable() Automatically track state changes and trigger component re-renders Minimal boilerplate, reactive programming model
Computed Values computed Derive values from observables, automatically cache and update Performance optimization, declarative derived state
Actions action Modify observable state, batch updates for performance Transactional updates, better debugging
Reactions autorun, reaction Side effects that run automatically when observables change Automatic synchronization, no manual subscriptions
MobX-State-Tree types.model() Runtime type system with snapshots, patches, time-travel Type safety, undo/redo, JSON serialization

Example: MobX Store with Computed Values

import { makeObservable, observable, computed, action } from 'mobx';
import { observer } from 'mobx-react-lite';

class TodoStore {
  todos = [];
  
  constructor() {
    makeObservable(this, {
      todos: observable,
      completedCount: computed,
      addTodo: action,
      toggleTodo: action
    });
  }
  
  get completedCount() {
    // Automatically recomputed when todos change
    return this.todos.filter(todo => todo.completed).length;
  }
  
  addTodo(text) {
    this.todos.push({ id: Date.now(), text, completed: false });
  }
  
  toggleTodo(id) {
    const todo = this.todos.find(t => t.id === id);
    if (todo) todo.completed = !todo.completed;
  }
}

const todoStore = new TodoStore();

// Observer component - re-renders only when used observables change
const TodoList = observer(() => {
  return (
    <div>
      <p>Completed: {todoStore.completedCount}/{todoStore.todos.length}</p>
      {todoStore.todos.map(todo => (
        <div key={todo.id} onClick={() => todoStore.toggleTodo(todo.id)}>
          {todo.text}
        </div>
      ))}
    </div>
  );
});
MobX Philosophy: Anything that can be derived from application state should be derived automatically. Focus on what to track, not how to track it.

5. SWR Recoil Data Fetching

Library Core Hook Key Feature Use Case
SWR NEW useSWR(key, fetcher) Stale-While-Revalidate strategy, automatic revalidation on focus/network Data fetching with smart caching, real-time updates
Recoil atom(), selector() Atomic state management with derived state, asynchronous queries Complex state graphs, React-centric state management
SWR Mutations useSWRMutation() Trigger remote mutations with automatic cache updates POST/PUT/DELETE with optimistic UI updates
Recoil Atoms useRecoilState() Independent state units, minimal re-renders, shared across components Fine-grained state subscriptions, component isolation
Recoil Selectors selector({ get }) Pure functions deriving state from atoms, async data fetching Computed values, GraphQL queries, API integration

SWR Example

import useSWR from 'swr';

const fetcher = url => fetch(url).then(r => r.json());

function Profile() {
  const { data, error, isLoading, mutate } = useSWR(
    '/api/user',
    fetcher,
    {
      revalidateOnFocus: true,
      revalidateOnReconnect: true,
      refreshInterval: 30000 // Poll every 30s
    }
  );
  
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error!</div>;
  
  return (
    <div>
      <h1>{data.name}</h1>
      <button onClick={() => mutate()}>
        Refresh
      </button>
    </div>
  );
}

// Optimistic update
import { useSWRConfig } from 'swr';

function UpdateButton() {
  const { mutate } = useSWRConfig();
  
  const updateUser = async () => {
    // Optimistic update
    mutate('/api/user', { name: 'New' }, false);
    // Remote update
    await fetch('/api/user', { method: 'PATCH' });
    // Revalidate
    mutate('/api/user');
  };
}

Recoil Example

import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';

// Atom - independent state unit
const textState = atom({
  key: 'textState',
  default: ''
});

// Selector - derived state
const charCountState = selector({
  key: 'charCountState',
  get: ({ get }) => {
    const text = get(textState);
    return text.length;
  }
});

// Async selector
const userState = selector({
  key: 'userState',
  get: async ({ get }) => {
    const userId = get(currentUserIdState);
    const response = await fetch(`/api/user/${userId}`);
    return response.json();
  }
});

function TextInput() {
  const [text, setText] = useRecoilState(textState);
  const count = useRecoilValue(charCountState);
  
  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <p>Character count: {count}</p>
    </div>
  );
}

SWR vs Recoil Comparison

  • SWR: Focused on data fetching, automatic revalidation, 5KB bundle
  • Recoil: General state management, atom-based architecture, React-centric
  • When to use SWR: API-driven apps, real-time data, caching strategy
  • When to use Recoil: Complex state graphs, derived state, async queries

6. TanStack Query React Query

Feature API Description Benefit
useQuery NEW useQuery({ queryKey, queryFn }) Declarative data fetching with automatic caching, background updates Zero-config caching, deduplication, stale-while-revalidate
useMutation useMutation({ mutationFn }) Perform create/update/delete operations with optimistic updates Automatic invalidation, rollback on error, loading states
Query Invalidation queryClient.invalidateQueries() Mark queries as stale to trigger automatic refetch Keep UI in sync after mutations, smart cache management
Parallel Queries useQueries() Execute multiple queries simultaneously, independent loading states Optimize data fetching, reduce waterfall requests
Infinite Queries useInfiniteQuery() Pagination with "load more" pattern, automatic page management Infinite scroll, cursor-based pagination
DevTools ReactQueryDevtools Inspect queries, cache state, refetch manually, time-travel Debugging cache behavior, performance optimization

Example: TanStack Query Complete Implementation

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// Query
function TodoList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['todos'],
    queryFn: async () => {
      const res = await fetch('/api/todos');
      return res.json();
    },
    staleTime: 5 * 60 * 1000, // 5 minutes
    cacheTime: 10 * 60 * 1000, // 10 minutes
    refetchOnWindowFocus: true,
    retry: 3
  });
  
  if (isLoading) return <Skeleton />;
  if (error) return <Error message={error.message} />;
  
  return <ul>{data.map(todo => <li>{todo.title}</li>)}</ul>;
}

// Mutation with optimistic update
function AddTodo() {
  const queryClient = useQueryClient();
  
  const mutation = useMutation({
    mutationFn: (newTodo) => fetch('/api/todos', {
      method: 'POST',
      body: JSON.stringify(newTodo)
    }),
    onMutate: async (newTodo) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: ['todos'] });
      
      // Snapshot previous value
      const previousTodos = queryClient.getQueryData(['todos']);
      
      // Optimistically update
      queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
      
      return { previousTodos };
    },
    onError: (err, newTodo, context) => {
      // Rollback on error
      queryClient.setQueryData(['todos'], context.previousTodos);
    },
    onSettled: () => {
      // Refetch after mutation
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    }
  });
  
  return <button onClick={() => mutation.mutate({ title: 'New' })}>Add</button>;
}

// Infinite Query
function InfiniteTodos() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage
  } = useInfiniteQuery({
    queryKey: ['todos'],
    queryFn: ({ pageParam = 0 }) => fetch(`/api/todos?page=${pageParam}`),
    getNextPageParam: (lastPage) => lastPage.nextCursor
  });
  
  return (
    <div>
      {data.pages.map(page => page.todos.map(todo => <div>{todo.title}</div>))}
      {hasNextPage && (
        <button onClick={fetchNextPage} disabled={isFetchingNextPage}>
          Load More
        </button>
      )}
    </div>
  );
}

Query States

State Description
isLoading First fetch, no cached data
isFetching Fetching (initial or background)
isSuccess Query successful, data available
isError Query failed, error available
isStale Data outdated, refetch pending

Cache Configuration

Option Purpose
staleTime How long data stays fresh
cacheTime Unused data retention time
refetchOnWindowFocus Refetch when window regains focus
refetchInterval Polling interval in milliseconds
retry Number of retry attempts
Why TanStack Query: Eliminates 90% of server state boilerplate, automatic background updates, built-in loading/error states, request deduplication, optimistic updates, and infinite queries out-of-the-box.