AI's Struggle with State Management Complexity

State management is where AI code generation most often falls apart. While AI can produce working components and basic CRUD operations, it consistently struggles with the architectural decisions that separate toy apps from production systems. The result: scattered state logic, prop drilling nightmares, unnecessary re-renders, and applications that become unmaintainable as they scale.

The state management landscape in 2025 has evolved significantly. According to recent surveys, Zustand has grown 30%+ year-over-year and now appears in approximately 40% of new React projects. Meanwhile, hand-written Redux has dropped to roughly 10% of new projects, though Redux Toolkit remains common in enterprise applications. React Context, while useful, introduces problems of "Provider Hell" and unstoppable re-renders when misused.

Research from Bit.dev shows that poor render performance can increase scripting time by 30-60%. When AI generates state management code without understanding your application's architecture, data flow requirements, and performance constraints, these problems compound quickly across your component tree.

AI State Management Statistics (2025)

  • Zustand adoption grew 30%+ year-over-year, now in ~40% of new React projects
  • Poor render performance can increase scripting time by 30-60%
  • AI defaults to prop drilling or God Context without architectural guidance
  • Hand-written Redux dropped to ~10% of new projects

Why AI Struggles with State Management

1. Missing Architectural Context

When you ask AI to "add user authentication state," it has no understanding of:

  • Your existing state management patterns
  • Which components need access to auth state
  • How frequently auth state changes
  • Whether you're using server-side rendering
  • Your team's preferences and expertise
  • Performance requirements for your user base

Without this context, AI defaults to the simplest patterns from its training data—often the wrong choice for your specific situation.

2. Training Data Bias

AI models are trained heavily on:

  • Tutorial code: Often uses prop drilling or Context for simplicity
  • Stack Overflow answers: Focus on solving immediate problems, not architecture
  • Outdated patterns: Legacy Redux patterns before Redux Toolkit
  • Small example apps: Don't require sophisticated state management

3. Single-Component Thinking

AI optimizes for generating working code in isolation. State management requires thinking about:

  • Component hierarchies and data flow
  • Which state is truly global vs. local
  • Server state vs. client state separation
  • Subscription patterns and re-render boundaries

This systems-level thinking is difficult to capture in a single prompt.

Common AI State Management Anti-Patterns

Anti-Pattern 1: The God Context

AI frequently generates a single Context that holds everything:

// AI-generated anti-pattern: God Context
const AppContext = createContext();

export function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [cart, setCart] = useState([]);
  const [products, setProducts] = useState([]);
  const [orders, setOrders] = useState([]);
  const [notifications, setNotifications] = useState([]);
  const [filters, setFilters] = useState({});
  const [searchQuery, setSearchQuery] = useState('');
  const [isLoading, setIsLoading] = useState(false);

  // Every value change re-renders ALL consumers!
  return (
    <AppContext.Provider value={{
      user, setUser,
      theme, setTheme,
      cart, setCart,
      products, setProducts,
      orders, setOrders,
      notifications, setNotifications,
      filters, setFilters,
      searchQuery, setSearchQuery,
      isLoading, setIsLoading
    }}>
      {children}
    </AppContext.Provider>
  );
}

The problem: Every time ANY value changes, EVERY component consuming this context re-renders. The searchQuery changes on every keystroke, causing the entire app to re-render.

Anti-Pattern 2: Excessive Prop Drilling

// AI-generated: Prop drilling nightmare
function App() {
  const [user, setUser] = useState(null);

  return (
    <Layout user={user} setUser={setUser}>
      <Dashboard user={user} setUser={setUser} />
    </Layout>
  );
}

function Layout({ user, setUser, children }) {
  return (
    <div>
      <Header user={user} setUser={setUser} />
      <Sidebar user={user} />
      {children}
    </div>
  );
}

function Header({ user, setUser }) {
  return (
    <header>
      <Navigation user={user} />
      <UserMenu user={user} setUser={setUser} />
    </header>
  );
}

function UserMenu({ user, setUser }) {
  return (
    <div>
      <Avatar user={user} />
      <DropdownMenu user={user} setUser={setUser} />
    </div>
  );
}

// ... continues through 10+ levels

The problem: Components become tightly coupled, difficult to refactor, and intermediate components carry props they don't use.

Anti-Pattern 3: Over-Lifted State

// AI-generated: State lifted too high
function ProductPage() {
  // All state at the top level
  const [searchTerm, setSearchTerm] = useState('');
  const [sortBy, setSortBy] = useState('price');
  const [filterCategory, setFilterCategory] = useState('all');
  const [selectedProduct, setSelectedProduct] = useState(null);
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [modalContent, setModalContent] = useState(null);

  // Every state change re-renders the entire page!
  return (
    <div>
      <SearchBar searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
      <Filters
        sortBy={sortBy}
        setSortBy={setSortBy}
        filterCategory={filterCategory}
        setFilterCategory={setFilterCategory}
      />
      <ProductGrid
        searchTerm={searchTerm}
        sortBy={sortBy}
        filterCategory={filterCategory}
        selectedProduct={selectedProduct}
        setSelectedProduct={setSelectedProduct}
      />
      <Modal isOpen={isModalOpen} setIsOpen={setIsModalOpen} content={modalContent} />
    </div>
  );
}

The fix: Keep state as local as possible. Modal state belongs in the Modal component. Search term can be local to SearchBar with debounced callback.

Anti-Pattern 4: Scattered State Logic

// AI-generated: No clear structure
// File: components/Cart.jsx
function Cart() {
  const [items, setItems] = useState([]);
  const [total, setTotal] = useState(0);

  useEffect(() => {
    // Duplicate calculation logic
    setTotal(items.reduce((sum, item) => sum + item.price * item.qty, 0));
  }, [items]);

  // Direct API call in component
  const addItem = async (product) => {
    const response = await fetch('/api/cart', {
      method: 'POST',
      body: JSON.stringify(product)
    });
    const data = await response.json();
    setItems(data.items);
  };
}

// File: components/CartSummary.jsx
function CartSummary() {
  const [items, setItems] = useState([]); // Duplicate state!
  const [total, setTotal] = useState(0); // Duplicate calculation!

  useEffect(() => {
    // Same fetch logic duplicated
    fetch('/api/cart').then(r => r.json()).then(setItems);
  }, []);
}

The problem: State is duplicated across components, leading to synchronization bugs. API logic is scattered instead of centralized.

Anti-Pattern 5: Derived State with useEffect

// AI-generated: Unnecessary useEffect for derived state
function ProductList({ products, filterCategory }) {
  const [filteredProducts, setFilteredProducts] = useState([]);

  // Anti-pattern: Using useEffect for derived state
  useEffect(() => {
    setFilteredProducts(
      products.filter(p => p.category === filterCategory)
    );
  }, [products, filterCategory]);

  return <Grid items={filteredProducts} />;
}

// CORRECT: Calculate during render
function ProductList({ products, filterCategory }) {
  // Derived state - no useEffect needed
  const filteredProducts = useMemo(
    () => products.filter(p => p.category === filterCategory),
    [products, filterCategory]
  );

  return <Grid items={filteredProducts} />;
}

2025 State Management Landscape

Understanding the current ecosystem helps you guide AI toward appropriate solutions:

Key Insight from Redux Maintainer: Mark Erikson emphasizes: "React Context is not a full state management solution for complex scenarios. Context lacks things like structured updates, middleware, devtools, etc." Use the right tool for the job.

State management solutions and their best use cases:

  • useState/useReducer: Local component state (100% adoption, excellent AI familiarity)
  • React Context: Theme, auth, locale - infrequent updates (~70% of projects)
  • TanStack Query: Server state, API data (~60% of projects)
  • Zustand: Shared client state, fine-grained subscriptions (~40% of projects)
  • Jotai: Atomic state, complex interdependencies (~15% of projects)
  • Redux Toolkit: Enterprise, large teams, complex workflows (~25% of projects)
  • MobX: Observable patterns, automatic tracking (~10% of projects)

State Management Decision Framework

Use this framework BEFORE prompting AI to generate state management code:

Step 1: Categorize Your State

// State Categories Decision Tree

/*
┌─────────────────────────────────────────────────────────┐
│                    Is the data...                        │
└─────────────────────────────────────────────────────────┘
                           │
           ┌───────────────┴───────────────┐
           ▼                               ▼
    From the server?                 Client-only?
           │                               │
           ▼                               │
  ┌────────────────┐          ┌────────────┴────────────┐
  │ React Query /  │          ▼                         ▼
  │ TanStack Query │    Shared across          Used by single
  │ SWR            │    components?             component?
  └────────────────┘          │                         │
                              ▼                         ▼
                   ┌──────────┴──────────┐      ┌──────────────┐
                   ▼                     ▼      │  useState/   │
            Updates           Updates          │  useReducer  │
            frequently?       infrequently?    └──────────────┘
                   │                     │
                   ▼                     ▼
           ┌──────────────┐     ┌──────────────┐
           │ Zustand /    │     │ React        │
           │ Jotai /      │     │ Context      │
           │ Redux        │     └──────────────┘
           └──────────────┘
*/

Step 2: Identify Update Patterns

// State Update Frequency Guide

const stateFrequencyGuide = {
  // HIGH FREQUENCY - Don't use Context
  highFrequency: {
    examples: ['search input', 'form fields', 'mouse position', 'animations'],
    solution: 'Local state or Zustand with selectors',
    avoid: 'React Context (causes re-render storms)'
  },

  // MEDIUM FREQUENCY - Consider carefully
  mediumFrequency: {
    examples: ['filter selections', 'pagination', 'sort options'],
    solution: 'Zustand, Jotai, or colocated state',
    avoid: 'Global Context without splitting'
  },

  // LOW FREQUENCY - Context is fine
  lowFrequency: {
    examples: ['theme', 'locale', 'auth user', 'feature flags'],
    solution: 'React Context (split by concern)',
    avoid: 'Over-engineering with Redux'
  }
};

Step 3: Map Component Access

// Before choosing state solution, map which components need access

const stateAccessMap = {
  userAuth: {
    needsRead: ['Header', 'Sidebar', 'ProtectedRoute', 'ProfileMenu'],
    needsWrite: ['LoginForm', 'LogoutButton'],
    updateFrequency: 'low',
    recommendation: 'Context (AuthContext)'
  },

  cartItems: {
    needsRead: ['CartIcon', 'CartPage', 'Checkout', 'ProductCard'],
    needsWrite: ['ProductCard', 'CartPage', 'Checkout'],
    updateFrequency: 'medium',
    recommendation: 'Zustand (fine-grained subscriptions)'
  },

  searchFilters: {
    needsRead: ['ProductGrid', 'FilterSidebar', 'ActiveFilters'],
    needsWrite: ['FilterSidebar', 'SearchBar'],
    updateFrequency: 'high',
    recommendation: 'URL state + local state (not Context)'
  }
};

Architecture-First Prompting for State

Don't let AI decide your state architecture. Make the decision first, then prompt accordingly:

Bad Prompt (Lets AI Decide Architecture)

// DON'T: Vague prompts produce anti-patterns
"Add cart functionality to my React app"

Good Prompt (Architecture-First)

"Create a cart feature using Zustand with the following architecture:

STATE STRUCTURE:
- Store: src/stores/cartStore.ts
- Items: array of { productId, quantity, price }
- Computed: totalItems, totalPrice (derived, not stored)

ACTIONS:
- addItem(product): Add or increment quantity
- removeItem(productId): Remove item completely
- updateQuantity(productId, quantity): Set specific quantity
- clearCart(): Remove all items

SELECTORS (for fine-grained subscriptions):
- useCartItems(): Returns items array
- useCartTotal(): Returns computed total
- useCartItemCount(): Returns total quantity
- useCartItem(productId): Returns specific item or undefined

PERSISTENCE:
- Persist to localStorage using zustand/middleware
- Rehydrate on app load

CONSTRAINTS:
- Components should only subscribe to what they need
- CartIcon only needs item count
- CartPage needs full items array
- Individual CartItem only needs its own data

Generate the store, selectors, and example usage."

Context-Specific Prompt

"Create a theme Context with these constraints:

SPLIT CONTEXTS (prevent unnecessary re-renders):
1. ThemeContext: { theme: 'light' | 'dark' }
2. ThemeDispatchContext: { setTheme: (theme) => void }

WHY SPLIT:
- Components reading theme don't re-render when setTheme reference changes
- Components only calling setTheme don't re-render on theme change

IMPLEMENTATION:
- Custom hooks: useTheme(), useSetTheme()
- Persist to localStorage
- Respect system preference initially
- Provide at app root

Generate provider, hooks, and usage example."

Architecture Decision Records for State

Document your state management decisions before implementation:

# ADR-003: State Management Architecture

## Status
Accepted

## Context
Building an e-commerce application with:
- ~50 components
- Product catalog (server state)
- Shopping cart (client state, persisted)
- User authentication (client state, persisted)
- UI preferences (client state)
- Filters/search (URL state)

## Decision

### Server State: TanStack Query
- Product listings, user data, orders
- Automatic caching, background refetch
- Optimistic updates for mutations

### Client State: Zustand
- Shopping cart (persisted to localStorage)
- UI state shared across distant components
- Fine-grained selectors to prevent re-renders

### Auth State: React Context
- User session (low-frequency updates)
- Split into AuthContext + AuthDispatchContext
- Rehydrate from secure cookie on load

### URL State: nuqs or native URLSearchParams
- Search filters, pagination, sort order
- Enables shareable/bookmarkable URLs
- SSR-friendly

### Local State: useState/useReducer
- Form inputs, modals, component-specific UI
- Don't lift unless truly needed

## Consequences
- Team must understand when to use each tool
- Clear boundaries prevent state duplication
- Performance optimized through proper subscriptions
- Code review checklist includes state placement

Performance Profiling for State Issues

React DevTools Profiler

Use the React DevTools Profiler to identify unnecessary re-renders:

// Enable "Record why each component rendered" in Profiler settings

// Common causes revealed by Profiler:
// 1. "Props changed" - often new object/array references
// 2. "Parent component rendered" - over-lifted state
// 3. "Context changed" - God Context anti-pattern
// 4. "Hooks changed" - unstable references in custom hooks

Why-Did-You-Render Integration

// wdyr.js - Development only
import React from 'react';

if (process.env.NODE_ENV === 'development') {
  const whyDidYouRender = require('@welldone-software/why-did-you-render');
  whyDidYouRender(React, {
    trackAllPureComponents: true,
    trackHooks: true,
    logOnDifferentValues: true,
  });
}

// In your component file:
function CartIcon({ itemCount }) {
  return <span>{itemCount}</span>;
}

CartIcon.whyDidYouRender = true; // Enable tracking for this component

// Console will log when CartIcon re-renders unnecessarily

Performance Monitoring Setup

// utils/performanceMonitor.js
export function measureRenderTime(componentName) {
  if (process.env.NODE_ENV !== 'development') return () => {};

  const start = performance.now();

  return () => {
    const duration = performance.now() - start;
    if (duration > 16) { // Longer than one frame (60fps)
      console.warn(
        `[Performance] ${componentName} render took ${duration.toFixed(2)}ms`
      );
    }
  };
}

// Usage in component
function ExpensiveComponent() {
  const endMeasure = measureRenderTime('ExpensiveComponent');

  // ... component logic

  useEffect(() => {
    endMeasure();
  });

  return <div>...</div>;
}

Zustand Best Practices

Zustand is increasingly popular but AI often generates suboptimal patterns:

Proper Zustand Store Structure

// stores/cartStore.ts
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';

interface CartItem {
  productId: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartState {
  items: CartItem[];
  // Actions
  addItem: (product: Omit<CartItem, 'quantity'>) => void;
  removeItem: (productId: string) => void;
  updateQuantity: (productId: string, quantity: number) => void;
  clearCart: () => void;
}

export const useCartStore = create<CartState>()(
  devtools(
    persist(
      immer((set, get) => ({
        items: [],

        addItem: (product) => set((state) => {
          const existing = state.items.find(
            item => item.productId === product.productId
          );
          if (existing) {
            existing.quantity += 1;
          } else {
            state.items.push({ ...product, quantity: 1 });
          }
        }),

        removeItem: (productId) => set((state) => {
          state.items = state.items.filter(
            item => item.productId !== productId
          );
        }),

        updateQuantity: (productId, quantity) => set((state) => {
          const item = state.items.find(
            item => item.productId === productId
          );
          if (item) {
            if (quantity <= 0) {
              state.items = state.items.filter(
                i => i.productId !== productId
              );
            } else {
              item.quantity = quantity;
            }
          }
        }),

        clearCart: () => set({ items: [] }),
      })),
      { name: 'cart-storage' }
    ),
    { name: 'CartStore' }
  )
);

// SELECTORS - Components subscribe only to what they need
export const useCartItems = () => useCartStore((state) => state.items);
export const useCartActions = () => useCartStore((state) => ({
  addItem: state.addItem,
  removeItem: state.removeItem,
  updateQuantity: state.updateQuantity,
  clearCart: state.clearCart,
}));

// Derived selectors (computed values)
export const useCartTotal = () => useCartStore((state) =>
  state.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
);

export const useCartItemCount = () => useCartStore((state) =>
  state.items.reduce((sum, item) => sum + item.quantity, 0)
);

export const useCartItem = (productId: string) => useCartStore((state) =>
  state.items.find(item => item.productId === productId)
);

Component Usage with Selectors

// components/CartIcon.tsx
// Only re-renders when item count changes
function CartIcon() {
  const itemCount = useCartItemCount();

  return (
    <button className="cart-icon">
      <ShoppingCartIcon />
      {itemCount > 0 && <span className="badge">{itemCount}</span>}
    </button>
  );
}

// components/CartTotal.tsx
// Only re-renders when total changes
function CartTotal() {
  const total = useCartTotal();

  return <span className="cart-total">${total.toFixed(2)}</span>;
}

// components/AddToCartButton.tsx
// Never re-renders from state changes (only uses actions)
function AddToCartButton({ product }) {
  const { addItem } = useCartActions();

  return (
    <button onClick={() => addItem(product)}>
      Add to Cart
    </button>
  );
}

Redux Toolkit Modern Patterns

When AI generates Redux code, ensure it uses RTK patterns, not legacy Redux:

RTK Slice with Async Thunks

// features/products/productsSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from '../../store';

interface Product {
  id: string;
  name: string;
  price: number;
  category: string;
}

interface ProductsState {
  items: Product[];
  status: 'idle' | 'loading' | 'succeeded' | 'failed';
  error: string | null;
  filters: {
    category: string | null;
    minPrice: number | null;
    maxPrice: number | null;
  };
}

const initialState: ProductsState = {
  items: [],
  status: 'idle',
  error: null,
  filters: {
    category: null,
    minPrice: null,
    maxPrice: null,
  },
};

// Async thunk with proper typing
export const fetchProducts = createAsyncThunk(
  'products/fetchProducts',
  async (_, { rejectWithValue }) => {
    try {
      const response = await fetch('/api/products');
      if (!response.ok) {
        throw new Error('Failed to fetch products');
      }
      return await response.json() as Product[];
    } catch (error) {
      return rejectWithValue(
        error instanceof Error ? error.message : 'Unknown error'
      );
    }
  }
);

const productsSlice = createSlice({
  name: 'products',
  initialState,
  reducers: {
    setFilter: (
      state,
      action: PayloadAction<Partial<ProductsState['filters']>>
    ) => {
      state.filters = { ...state.filters, ...action.payload };
    },
    clearFilters: (state) => {
      state.filters = initialState.filters;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchProducts.pending, (state) => {
        state.status = 'loading';
        state.error = null;
      })
      .addCase(fetchProducts.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.items = action.payload;
      })
      .addCase(fetchProducts.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.payload as string;
      });
  },
});

export const { setFilter, clearFilters } = productsSlice.actions;

// Memoized selectors
export const selectAllProducts = (state: RootState) => state.products.items;
export const selectProductsStatus = (state: RootState) => state.products.status;
export const selectProductsError = (state: RootState) => state.products.error;

// Derived selector with filtering
export const selectFilteredProducts = (state: RootState) => {
  const { items, filters } = state.products;

  return items.filter((product) => {
    if (filters.category && product.category !== filters.category) {
      return false;
    }
    if (filters.minPrice && product.price < filters.minPrice) {
      return false;
    }
    if (filters.maxPrice && product.price > filters.maxPrice) {
      return false;
    }
    return true;
  });
};

export default productsSlice.reducer;

RTK Query Alternative

// services/api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

interface Product {
  id: string;
  name: string;
  price: number;
}

export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Product', 'Cart'],
  endpoints: (builder) => ({
    getProducts: builder.query<Product[], void>({
      query: () => 'products',
      providesTags: ['Product'],
    }),

    getProduct: builder.query<Product, string>({
      query: (id) => `products/${id}`,
      providesTags: (result, error, id) => [{ type: 'Product', id }],
    }),

    updateProduct: builder.mutation<Product, Partial<Product> & { id: string }>({
      query: ({ id, ...patch }) => ({
        url: `products/${id}`,
        method: 'PATCH',
        body: patch,
      }),
      // Optimistic update
      async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
        const patchResult = dispatch(
          api.util.updateQueryData('getProduct', id, (draft) => {
            Object.assign(draft, patch);
          })
        );
        try {
          await queryFulfilled;
        } catch {
          patchResult.undo();
        }
      },
      invalidatesTags: (result, error, { id }) => [{ type: 'Product', id }],
    }),
  }),
});

export const {
  useGetProductsQuery,
  useGetProductQuery,
  useUpdateProductMutation,
} = api;

Testing State Management Logic

Testing Zustand Stores

// __tests__/cartStore.test.ts
import { useCartStore } from '../stores/cartStore';

describe('Cart Store', () => {
  beforeEach(() => {
    // Reset store before each test
    useCartStore.setState({ items: [] });
  });

  describe('addItem', () => {
    it('adds new item to empty cart', () => {
      const { addItem } = useCartStore.getState();

      addItem({ productId: '1', name: 'Test Product', price: 10 });

      const { items } = useCartStore.getState();
      expect(items).toHaveLength(1);
      expect(items[0]).toEqual({
        productId: '1',
        name: 'Test Product',
        price: 10,
        quantity: 1,
      });
    });

    it('increments quantity for existing item', () => {
      const { addItem } = useCartStore.getState();

      addItem({ productId: '1', name: 'Test Product', price: 10 });
      addItem({ productId: '1', name: 'Test Product', price: 10 });

      const { items } = useCartStore.getState();
      expect(items).toHaveLength(1);
      expect(items[0].quantity).toBe(2);
    });
  });

  describe('selectors', () => {
    it('calculates total correctly', () => {
      useCartStore.setState({
        items: [
          { productId: '1', name: 'A', price: 10, quantity: 2 },
          { productId: '2', name: 'B', price: 5, quantity: 3 },
        ],
      });

      // Test selector directly
      const total = useCartStore.getState().items.reduce(
        (sum, item) => sum + item.price * item.quantity, 0
      );
      expect(total).toBe(35); // (10*2) + (5*3)
    });
  });
});

Testing Redux Slices

// __tests__/productsSlice.test.ts
import reducer, {
  setFilter,
  clearFilters,
  fetchProducts,
} from '../features/products/productsSlice';

describe('products slice', () => {
  const initialState = {
    items: [],
    status: 'idle',
    error: null,
    filters: { category: null, minPrice: null, maxPrice: null },
  };

  describe('reducers', () => {
    it('should handle setFilter', () => {
      const state = reducer(initialState, setFilter({ category: 'electronics' }));
      expect(state.filters.category).toBe('electronics');
    });

    it('should handle clearFilters', () => {
      const stateWithFilters = {
        ...initialState,
        filters: { category: 'electronics', minPrice: 10, maxPrice: 100 },
      };
      const state = reducer(stateWithFilters, clearFilters());
      expect(state.filters).toEqual(initialState.filters);
    });
  });

  describe('async thunks', () => {
    it('should set loading state when fetch is pending', () => {
      const state = reducer(initialState, fetchProducts.pending('requestId'));
      expect(state.status).toBe('loading');
      expect(state.error).toBeNull();
    });

    it('should set items when fetch succeeds', () => {
      const products = [{ id: '1', name: 'Test', price: 10, category: 'test' }];
      const state = reducer(
        initialState,
        fetchProducts.fulfilled(products, 'requestId')
      );
      expect(state.status).toBe('succeeded');
      expect(state.items).toEqual(products);
    });

    it('should set error when fetch fails', () => {
      const state = reducer(
        initialState,
        fetchProducts.rejected(null, 'requestId', undefined, 'Network error')
      );
      expect(state.status).toBe('failed');
      expect(state.error).toBe('Network error');
    });
  });
});

Key Takeaways

State Management Essentials

  • Decide Architecture Before Prompting: Don't let AI choose your state management solution—use the decision framework to categorize your state first
  • Avoid the God Context Anti-Pattern: Split Context by concern, or use Zustand/Jotai for fine-grained subscriptions
  • Keep State as Local as Possible: Over-lifting state causes over-rendering—poor render performance can increase scripting time by 30-60%
  • Separate Server State from Client State: Use TanStack Query or RTK Query for server data, Zustand/Redux for client-only state
  • Use Selectors for Fine-Grained Subscriptions: Components should subscribe only to the state they need
  • Profile Before Optimizing: Use React DevTools Profiler and why-did-you-render to identify actual problems

Conclusion

State management is where AI assistance requires the most human guidance. AI excels at implementing patterns once you've decided on the architecture, but it cannot make architectural decisions for you. It doesn't understand your component hierarchy, performance requirements, or team expertise.

The key is to think architecture first, then prompt specifically. Use the decision framework to categorize your state. Document decisions in ADRs. Then prompt AI with explicit constraints about state structure, selectors, and update patterns.

Remember: The goal isn't to avoid AI assistance—it's to provide enough context that AI generates maintainable, performant state management code.

In our next article, we'll explore Browser API Limitations: When AI Assumes Features That Don't Exist, examining how AI generates code for non-existent or deprecated browser APIs.