You are viewing a preview of this lesson. Sign in to start learning
Back to React

State Management Solutions

Manage complex application state beyond useContext

State Management Solutions in React

Master React state management with free flashcards and spaced repetition practice. This comprehensive lesson covers the Context API, Redux, Zustand, and modern state management patternsβ€”essential tools for building scalable React applications. You'll learn when to use each solution, implement practical examples, and avoid common pitfalls that plague React developers.

Welcome to Advanced State Management! 🎯

State management is one of the most critical aspects of building React applications. As your app grows beyond a few components, managing state effectively becomes challenging. Should you lift state up? Use Context? Reach for Redux? This lesson will guide you through the ecosystem of state management solutions, helping you choose the right tool for each scenario.

What You'll Learn:

  • πŸ”„ Understanding different types of state (local, global, server)
  • 🌐 Context API for prop drilling elimination
  • πŸ“¦ Redux architecture and modern Redux Toolkit
  • ⚑ Zustand for lightweight global state
  • 🎨 Choosing the right solution for your needs

πŸ’‘ Pro Tip: The best state management solution is often the simplest one that solves your problem. Don't reach for Redux if Context API will do!


Core Concepts: Understanding State in React πŸ’»

Types of State in React Applications

Before diving into solutions, let's categorize state:

State Type Description Examples Best Tool
Local State Data needed by a single component Form inputs, toggles, counters useState, useReducer
Shared State Data shared between components User info, theme, language Context API, Zustand
Remote/Server State Data fetched from APIs User profiles, product lists React Query, SWR
URL State Data stored in the URL Filters, pagination, search React Router, Next.js router

🧠 Memory Device: Think "LSRU" - Local, Shared, Remote, URL

The Context API: React's Built-in Solution 🌐

Context API solves prop drilling - the problem of passing props through many layers of components. It's built into React and perfect for sharing data like themes, auth status, or user preferences.

How Context Works:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Context Provider            β”‚
β”‚    (Holds the state/value)          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
    β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”
    β”‚           β”‚
β”Œβ”€β”€β”€β–Όβ”€β”€β”€β”   β”Œβ”€β”€β”€β–Όβ”€β”€β”€β”
β”‚Child Aβ”‚   β”‚Child Bβ”‚
β””β”€β”€β”€β”¬β”€β”€β”€β”˜   β””β”€β”€β”€β”¬β”€β”€β”€β”˜
    β”‚           β”‚
β”Œβ”€β”€β”€β–Όβ”€β”€β”€β”   β”Œβ”€β”€β”€β–Όβ”€β”€β”€β”
β”‚Child Cβ”‚   β”‚Child Dβ”‚  ← All can access
β””β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”˜     context value

Key Characteristics:

  • βœ… Built into React (no dependencies)
  • βœ… Simple API (createContext, Provider, useContext)
  • ⚠️ Can cause unnecessary re-renders if not optimized
  • ⚠️ Not ideal for frequently changing state

Redux: The Enterprise Standard πŸ“¦

Redux implements a unidirectional data flow pattern with a single source of truth. Modern Redux uses Redux Toolkit (RTK) which dramatically simplifies the setup.

Redux Architecture:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  dispatch   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚Component │──────────→  β”‚  Action  β”‚
β””β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”˜             β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
     β”‚                         β”‚
     β”‚                         β–Ό
     β”‚                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚                   β”‚ Reducer  β”‚
     β”‚                   β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
     β”‚                         β”‚
     β”‚    subscribe            β–Ό
     β”‚                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     └───────────────────│  Store   β”‚
                         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          Single source of truth

When to Use Redux:

  • 🏒 Large applications with complex state logic
  • πŸ”„ State needs to be accessible across many components
  • πŸ“ You need powerful debugging (Redux DevTools)
  • πŸ§ͺ Predictable state updates are critical

Zustand: The Lightweight Alternative ⚑

Zustand (German for "state") is a minimal state management library that's become increasingly popular. It uses hooks and requires minimal boilerplate.

Zustand Philosophy:

  • 🎯 Simple and unopinionated
  • πŸͺΆ Tiny bundle size (~1KB)
  • ⚑ No providers needed
  • 🎨 Works with React's Suspense

πŸ€” Did You Know?

Redux was inspired by Flux architecture from Facebook and Elm's architecture. The creator, Dan Abramov, built Redux as a learning exercise and it became the de facto standard for React state management!

Example 1: Context API for Theme Management 🎨

Let's build a theme switcher using Context API. This is a perfect use case - theme doesn't change frequently and needs to be accessible throughout the app.

import React, { createContext, useContext, useState } from 'react';

// 1. Create Context
const ThemeContext = createContext();

// 2. Create Provider Component
export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  };
  
  const value = {
    theme,
    toggleTheme
  };
  
  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. Create Custom Hook for Easy Access
export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

// 4. Use in Components
function Header() {
  const { theme, toggleTheme } = useTheme();
  
  return (
    <header style={{ 
      background: theme === 'light' ? '#fff' : '#333',
      color: theme === 'light' ? '#000' : '#fff'
    }}>
      <h1>My App</h1>
      <button onClick={toggleTheme}>
        Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
      </button>
    </header>
  );
}

// 5. Wrap App with Provider
function App() {
  return (
    <ThemeProvider>
      <Header />
      {/* Other components */}
    </ThemeProvider>
  );
}

Key Points:

  • πŸ—οΈ Separation of concerns: Provider logic is isolated
  • 🎣 Custom hook: useTheme() provides clean API and error handling
  • πŸ”’ Error boundary: Throws error if used outside provider
  • πŸ“¦ Value object: Pass multiple values/functions together

πŸ’‘ Performance Tip: If your context value changes frequently, split into multiple contexts to prevent unnecessary re-renders!


Example 2: Redux Toolkit for Shopping Cart πŸ›’

Let's implement a shopping cart using modern Redux Toolkit. This demonstrates Redux's strength with complex state logic.

import { configureStore, createSlice } from '@reduxjs/toolkit';
import { useSelector, useDispatch, Provider } from 'react-redux';

// 1. Create Slice (combines actions + reducer)
const cartSlice = createSlice({
  name: 'cart',
  initialState: {
    items: [],
    total: 0
  },
  reducers: {
    addItem: (state, action) => {
      const existingItem = state.items.find(
        item => item.id === action.payload.id
      );
      
      if (existingItem) {
        existingItem.quantity += 1;
      } else {
        state.items.push({ ...action.payload, quantity: 1 });
      }
      
      // Redux Toolkit uses Immer - direct mutation is safe!
      state.total += action.payload.price;
    },
    
    removeItem: (state, action) => {
      const item = state.items.find(item => item.id === action.payload);
      if (item) {
        state.total -= item.price * item.quantity;
        state.items = state.items.filter(item => item.id !== action.payload);
      }
    },
    
    updateQuantity: (state, action) => {
      const { id, quantity } = action.payload;
      const item = state.items.find(item => item.id === id);
      
      if (item) {
        state.total -= item.price * item.quantity;
        item.quantity = quantity;
        state.total += item.price * quantity;
      }
    },
    
    clearCart: (state) => {
      state.items = [];
      state.total = 0;
    }
  }
});

// 2. Export Actions
export const { addItem, removeItem, updateQuantity, clearCart } = cartSlice.actions;

// 3. Create Store
const store = configureStore({
  reducer: {
    cart: cartSlice.reducer
  }
});

// 4. Use in Components
function ProductCard({ product }) {
  const dispatch = useDispatch();
  
  return (
    <div className="product-card">
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button onClick={() => dispatch(addItem(product))}>
        Add to Cart
      </button>
    </div>
  );
}

function Cart() {
  const { items, total } = useSelector(state => state.cart);
  const dispatch = useDispatch();
  
  return (
    <div className="cart">
      <h2>Shopping Cart</h2>
      {items.map(item => (
        <div key={item.id} className="cart-item">
          <span>{item.name}</span>
          <input
            type="number"
            value={item.quantity}
            onChange={(e) => dispatch(updateQuantity({
              id: item.id,
              quantity: parseInt(e.target.value)
            }))}
          />
          <button onClick={() => dispatch(removeItem(item.id))}>
            Remove
          </button>
        </div>
      ))}
      <div className="cart-total">
        <strong>Total: ${total.toFixed(2)}</strong>
      </div>
      <button onClick={() => dispatch(clearCart())}>
        Clear Cart
      </button>
    </div>
  );
}

// 5. Wrap App
function App() {
  return (
    <Provider store={store}>
      {/* App components */}
    </Provider>
  );
}

Redux Toolkit Advantages:

  • 🎯 Less boilerplate: No action types, action creators simplified
  • πŸ”§ Immer integration: Write "mutating" code safely
  • πŸ“¦ Built-in DevTools: Time-travel debugging included
  • ⚑ createAsyncThunk: Easy async action handling

🧠 Remember: Redux actions describe what happened, reducers decide how state changes.


Example 3: Zustand for Simple Global State ⚑

Zustand shines when you need simple global state without Redux's ceremony. Here's a user authentication store:

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

// 1. Create Store (no provider needed!)
const useAuthStore = create(
  persist(
    (set, get) => ({
      // State
      user: null,
      token: null,
      isAuthenticated: false,
      
      // Actions
      login: async (credentials) => {
        try {
          const response = await fetch('/api/login', {
            method: 'POST',
            body: JSON.stringify(credentials)
          });
          
          const data = await response.json();
          
          set({
            user: data.user,
            token: data.token,
            isAuthenticated: true
          });
        } catch (error) {
          console.error('Login failed:', error);
          throw error;
        }
      },
      
      logout: () => {
        set({
          user: null,
          token: null,
          isAuthenticated: false
        });
      },
      
      updateProfile: (updates) => {
        set(state => ({
          user: { ...state.user, ...updates }
        }));
      },
      
      // Derived state (computed values)
      getUserName: () => {
        const { user } = get();
        return user ? `${user.firstName} ${user.lastName}` : 'Guest';
      }
    }),
    {
      name: 'auth-storage', // localStorage key
      getStorage: () => localStorage // or sessionStorage
    }
  )
);

// 2. Use in Components (no Provider!)
function LoginForm() {
  const login = useAuthStore(state => state.login);
  const [credentials, setCredentials] = useState({ email: '', password: '' });
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      await login(credentials);
    } catch (error) {
      alert('Login failed');
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={credentials.email}
        onChange={(e) => setCredentials({ 
          ...credentials, 
          email: e.target.value 
        })}
      />
      <input
        type="password"
        value={credentials.password}
        onChange={(e) => setCredentials({ 
          ...credentials, 
          password: e.target.value 
        })}
      />
      <button type="submit">Login</button>
    </form>
  );
}

function UserProfile() {
  // Select only what you need (prevents unnecessary re-renders)
  const user = useAuthStore(state => state.user);
  const getUserName = useAuthStore(state => state.getUserName);
  const updateProfile = useAuthStore(state => state.updateProfile);
  
  if (!user) return <div>Please log in</div>;
  
  return (
    <div>
      <h2>Welcome, {getUserName()}!</h2>
      <p>Email: {user.email}</p>
      <button onClick={() => updateProfile({ theme: 'dark' })}>
        Switch to Dark Mode
      </button>
    </div>
  );
}

function Header() {
  // Subscribe to only isAuthenticated
  const isAuthenticated = useAuthStore(state => state.isAuthenticated);
  const logout = useAuthStore(state => state.logout);
  
  return (
    <header>
      {isAuthenticated ? (
        <button onClick={logout}>Logout</button>
      ) : (
        <a href="/login">Login</a>
      )}
    </header>
  );
}

Zustand Benefits:

  • 🎯 No boilerplate: Define state and actions in one place
  • 🚫 No provider: Use anywhere in your app immediately
  • 🎨 Selective subscriptions: Components only re-render when their slice changes
  • πŸ’Ύ Middleware support: Persist, devtools, immer built-in
  • πŸͺΆ Tiny: Minimal impact on bundle size

πŸ’‘ Performance Tip: Use selectors wisely! useAuthStore(state => state.user.email) is better than useAuthStore(state => state).user.email because it only re-renders when email changes.


Example 4: Combining Solutions - The Hybrid Approach πŸ”„

In real applications, you often combine multiple state management solutions. Here's a practical example:

import create from 'zustand';
import { useQuery, useMutation, QueryClient, QueryClientProvider } from 'react-query';
import { createContext, useContext, useState } from 'react';

// 1. LOCAL STATE: Component-specific (useState)
function SearchBar() {
  const [query, setQuery] = useState(''); // Only SearchBar needs this
  
  return (
    <input 
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Search..."
    />
  );
}

// 2. ZUSTAND: UI State (global, synchronous)
const useUIStore = create((set) => ({
  sidebarOpen: false,
  modalOpen: false,
  theme: 'light',
  
  toggleSidebar: () => set(state => ({ 
    sidebarOpen: !state.sidebarOpen 
  })),
  
  openModal: () => set({ modalOpen: true }),
  closeModal: () => set({ modalOpen: false }),
  
  setTheme: (theme) => set({ theme })
}));

// 3. REACT QUERY: Server State (async data)
function ProductList() {
  // React Query handles loading, error, caching, refetching
  const { data: products, isLoading, error } = useQuery(
    'products',
    async () => {
      const response = await fetch('/api/products');
      return response.json();
    },
    {
      staleTime: 5 * 60 * 1000, // Consider fresh for 5 minutes
      cacheTime: 10 * 60 * 1000 // Keep in cache for 10 minutes
    }
  );
  
  const mutation = useMutation(
    (newProduct) => {
      return fetch('/api/products', {
        method: 'POST',
        body: JSON.stringify(newProduct)
      });
    },
    {
      onSuccess: () => {
        // Invalidate and refetch
        queryClient.invalidateQueries('products');
      }
    }
  );
  
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return (
    <div>
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

// 4. CONTEXT: Cross-cutting concerns (feature flags, i18n)
const FeatureFlagsContext = createContext();

function FeatureFlagsProvider({ children }) {
  const [flags] = useState({
    newCheckout: true,
    betaFeatures: false,
    aiRecommendations: true
  });
  
  return (
    <FeatureFlagsContext.Provider value={flags}>
      {children}
    </FeatureFlagsContext.Provider>
  );
}

function CheckoutButton() {
  const flags = useContext(FeatureFlagsContext);
  
  return flags.newCheckout ? (
    <NewCheckout />
  ) : (
    <OldCheckout />
  );
}

// 5. PUTTING IT ALL TOGETHER
const queryClient = new QueryClient();

function App() {
  const sidebarOpen = useUIStore(state => state.sidebarOpen);
  
  return (
    <QueryClientProvider client={queryClient}>
      <FeatureFlagsProvider>
        <div className={sidebarOpen ? 'with-sidebar' : ''}>
          <Sidebar />
          <main>
            <ProductList />
          </main>
        </div>
      </FeatureFlagsProvider>
    </QueryClientProvider>
  );
}

Decision Matrix:

Use Case Best Solution Why
Form inputs useState Component-specific, temporary
Theme, language Context API Rarely changes, tree-wide
UI state (modals, sidebar) Zustand Simple, synchronous, global
API data React Query/SWR Caching, revalidation, loading states
Complex business logic Redux Predictable, debuggable, time-travel
URL params React Router Shareable, bookmarkable state

🧠 Mental Model: Think of state as layers:

  • Bottom layer: Local state (useState)
  • Middle layer: Shared UI state (Context/Zustand)
  • Top layer: Server state (React Query)
  • Side layer: URL state (Router)

Common Mistakes & How to Avoid Them ⚠️

Mistake 1: Context for Everything

❌ Wrong:

// Using Context for frequently changing data
function App() {
  const [count, setCount] = useState(0);
  
  // Every component re-renders on count change!
  return (
    <CountContext.Provider value={{ count, setCount }}>
      <Header /> {/* Re-renders */}
      <Sidebar /> {/* Re-renders */}
      <Footer /> {/* Re-renders */}
      <Counter /> {/* Only this needs count! */}
    </CountContext.Provider>
  );
}

βœ… Right:

// Use local state or Zustand for frequent updates
const useCountStore = create((set) => ({
  count: 0,
  increment: () => set(state => ({ count: state.count + 1 }))
}));

function Counter() {
  // Only Counter re-renders
  const count = useCountStore(state => state.count);
  return <div>{count}</div>;
}

Why it matters: Context causes all consumers to re-render, even if they don't use the changed value.

Mistake 2: Not Memoizing Context Values

❌ Wrong:

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  // New object every render!
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

βœ… Right:

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const value = useMemo(
    () => ({ theme, setTheme }),
    [theme]
  );
  
  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

Mistake 3: Redux for Everything

❌ Wrong:

// Putting form state in Redux
const formSlice = createSlice({
  name: 'form',
  initialState: {
    firstName: '',
    lastName: '',
    email: ''
  },
  reducers: {
    updateFirstName: (state, action) => {
      state.firstName = action.payload;
    },
    // More boilerplate for simple local state...
  }
});

βœ… Right:

// Keep form state local
function ContactForm() {
  const [formData, setFormData] = useState({
    firstName: '',
    lastName: '',
    email: ''
  });
  
  // Only put in Redux when submitting
  const dispatch = useDispatch();
  const handleSubmit = () => {
    dispatch(submitContact(formData));
  };
}

Mistake 4: Not Splitting Redux State Properly

❌ Wrong:

// One giant reducer
const appReducer = createSlice({
  name: 'app',
  initialState: {
    users: [],
    products: [],
    orders: [],
    ui: {},
    // Everything in one place!
  }
});

βœ… Right:

// Split by domain
const store = configureStore({
  reducer: {
    users: usersReducer,
    products: productsReducer,
    orders: ordersReducer,
    ui: uiReducer
  }
});

Mistake 5: Mixing Server and Client State

❌ Wrong:

// Storing API data in Zustand/Redux
const useStore = create((set) => ({
  products: [],
  loading: false,
  
  fetchProducts: async () => {
    set({ loading: true });
    const data = await fetch('/api/products');
    set({ products: data, loading: false });
    // No caching, stale data, manual refetch logic!
  }
}));

βœ… Right:

// Use React Query for server state
function ProductList() {
  const { data, isLoading } = useQuery(
    'products',
    () => fetch('/api/products').then(r => r.json()),
    {
      staleTime: 5000,
      // Automatic caching, refetching, background updates!
    }
  );
}

Mistake 6: Over-selecting in Zustand

❌ Wrong:

// Component re-renders on ANY store change
function UserName() {
  const store = useStore(); // Gets entire store
  return <div>{store.user.name}</div>;
}

βœ… Right:

// Component only re-renders when name changes
function UserName() {
  const name = useStore(state => state.user.name);
  return <div>{name}</div>;
}

Performance Rule: Always select the smallest slice you need!


Key Takeaways 🎯

πŸ“‹ Quick Reference Card

Solution Best For Bundle Size Learning Curve
useState/useReducer Local component state 0KB (built-in) ⭐ Easy
Context API Infrequent global updates 0KB (built-in) ⭐⭐ Easy
Zustand Simple global state ~1KB ⭐⭐ Easy
Redux Toolkit Complex state logic ~11KB ⭐⭐⭐ Moderate
React Query Server/async state ~13KB ⭐⭐⭐ Moderate
Jotai/Recoil Atomic state ~3KB ⭐⭐⭐ Moderate

Decision Framework:

Start Here
    ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Is it local state?  β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
   YES β”‚ NO
       β”‚
       ↓               ↓
  useState      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
               β”‚ Is it server data?   β”‚
               β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                      β”‚
                  YES β”‚ NO
                      β”‚
                      ↓               ↓
               React Query    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                             β”‚ Is it complex?    β”‚
                             β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                    β”‚
                                YES β”‚ NO
                                    β”‚
                                    ↓          ↓
                                Redux     Context/Zustand

Golden Rules:

  1. 🎯 Start simple - Use useState until you can't
  2. πŸ”„ Separate concerns - Server state β‰  Client state
  3. ⚑ Optimize subscriptions - Select only what you need
  4. πŸ§ͺ Measure first - Profile before optimizing
  5. πŸ“¦ Consider bundle size - Every KB matters
  6. 🎨 Match patterns to problems - Don't force solutions

Essential Patterns to Remember

The Compound Component Pattern with Context:

// Powerful for building flexible APIs
const Tabs = ({ children }) => {
  const [activeTab, setActiveTab] = useState(0);
  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      {children}
    </TabsContext.Provider>
  );
};

Tabs.List = ({ children }) => <div role="tablist">{children}</div>;
Tabs.Tab = ({ index, children }) => {
  const { activeTab, setActiveTab } = useContext(TabsContext);
  return (
    <button 
      onClick={() => setActiveTab(index)}
      aria-selected={activeTab === index}
    >
      {children}
    </button>
  );
};

The Observer Pattern with Zustand:

// Subscribe to changes outside React
const unsubscribe = useStore.subscribe(
  (state) => state.user,
  (user) => {
    console.log('User changed:', user);
    // Sync with analytics, localStorage, etc.
  }
);

πŸ’‘ Pro Tip: When in doubt, check React's official recommendations. The React team now suggests React Query/SWR for server state and Zustand/Jotai for client state over Redux in many cases.


πŸ“š Further Study

Official Documentation:

Advanced Topics to Explore Next:

  • πŸ”„ State machines with XState
  • 🎯 Atomic state with Jotai and Recoil
  • πŸ“‘ Real-time state with WebSockets
  • πŸ§ͺ Testing state management logic
  • ⚑ Server components and React Server Components (RSC)