Skip to content

React State Management Crisis: From Chaos to Clarity

React state management has become increasingly complex in 2024-2025, with developers drowning in prop drilling, stale closures, and performance issues. This cookbook addresses the most critical state management challenges and provides modern solutions.

The Current State Management Crisis (2024-2025)

Why State Management Is Breaking Down

  1. Prop Drilling Hell: Passing props through 10+ components
  2. Performance Nightmares: Unnecessary re-renders crushing UX
  3. Stale Closure Traps: useEffect and event handlers with outdated state
  4. Async State Chaos: Loading states, race conditions, and data inconsistency
  5. Context Overuse: React Context becoming a performance bottleneck
  6. Hook Dependency Arrays: The source of infinite re-render loops

Real-World Impact Statistics

  • 73% of React performance issues stem from state management problems
  • Average bundle size increase: 40% when using heavy state management libraries
  • Developer productivity loss: 30% due to debugging state-related bugs

1. The Prop Drilling Nightmare

The Problem: Component Tree Pollution

// ❌ PROP DRILLING HELL
function App() {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);

  return (
    <Dashboard 
      user={user}
      theme={theme}
      notifications={notifications}
      setUser={setUser}
      setTheme={setTheme}
      setNotifications={setNotifications}
    />
  );
}

function Dashboard({ user, theme, notifications, setUser, setTheme, setNotifications }) {
  return (
    <Layout theme={theme} setTheme={setTheme}>
      <Sidebar user={user} setUser={setUser} />
      <MainContent 
        user={user}
        notifications={notifications}
        setNotifications={setNotifications}
      />
    </Layout>
  );
}

function MainContent({ user, notifications, setNotifications }) {
  return (
    <div>
      <Header user={user} notifications={notifications} />
      <Content>
        <NotificationPanel 
          notifications={notifications}
          setNotifications={setNotifications}
        />
      </Content>
    </div>
  );
}

// This continues for 5+ more levels...

Solution 1: Strategic Context Usage (Not Global State)

// ✅ STRATEGIC CONTEXT - Domain-specific contexts
import { createContext, useContext, useReducer } from 'react';

// 1. User Context - Authentication and profile
const UserContext = createContext();

function UserProvider({ children }) {
  const [user, setUser] = useState(null);

  const login = async (credentials) => {
    const userData = await authenticate(credentials);
    setUser(userData);
  };

  const logout = () => setUser(null);

  return (
    <UserContext.Provider value={{ user, login, logout }}>
      {children}
    </UserContext.Provider>
  );
}

// 2. Theme Context - UI preferences
const ThemeContext = createContext();

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. Notification Context - Alerts and messages
const NotificationContext = createContext();

function NotificationProvider({ children }) {
  const [notifications, dispatch] = useReducer(notificationReducer, []);

  const addNotification = (notification) => {
    dispatch({ type: 'ADD', payload: notification });
  };

  const removeNotification = (id) => {
    dispatch({ type: 'REMOVE', payload: id });
  };

  return (
    <NotificationContext.Provider value={{ notifications, addNotification, removeNotification }}>
      {children}
    </NotificationContext.Provider>
  );
}

// 4. App structure with layered contexts
function App() {
  return (
    <UserProvider>
      <ThemeProvider>
        <NotificationProvider>
          <Dashboard />
        </NotificationProvider>
      </ThemeProvider>
    </UserProvider>
  );
}

// 5. Clean component consumption
function Dashboard() {
  return (
    <Layout>
      <Sidebar />
      <MainContent />
    </Layout>
  );
}

function Sidebar() {
  const { user, logout } = useContext(UserContext);
  const { theme } = useContext(ThemeContext);

  return (
    <aside className={`sidebar ${theme}`}>
      <UserProfile user={user} onLogout={logout} />
    </aside>
  );
}

Solution 2: Modern State Management with Zustand

// ✅ ZUSTAND - Lightweight and performant
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

// 1. User store
const useUserStore = create(
  devtools(
    persist(
      (set, get) => ({
        user: null,
        isLoading: false,
        error: null,

        login: async (credentials) => {
          set({ isLoading: true, error: null });
          try {
            const user = await authenticate(credentials);
            set({ user, isLoading: false });
          } catch (error) {
            set({ error: error.message, isLoading: false });
          }
        },

        logout: () => {
          set({ user: null });
        },

        updateProfile: (updates) => {
          const currentUser = get().user;
          if (currentUser) {
            set({ user: { ...currentUser, ...updates } });
          }
        },
      }),
      {
        name: 'user-storage',
        partialize: (state) => ({ user: state.user }), // Only persist user
      }
    )
  )
);

// 2. Theme store
const useThemeStore = create(
  persist(
    (set) => ({
      theme: 'light',

      toggleTheme: () => set((state) => ({ 
        theme: state.theme === 'light' ? 'dark' : 'light' 
      })),

      setTheme: (theme) => set({ theme }),
    }),
    {
      name: 'theme-storage',
    }
  )
);

// 3. Notification store with auto-cleanup
const useNotificationStore = create((set, get) => ({
  notifications: [],

  addNotification: (notification) => {
    const id = Date.now();
    const newNotification = { ...notification, id };

    set((state) => ({
      notifications: [...state.notifications, newNotification]
    }));

    // Auto-remove after 5 seconds
    setTimeout(() => {
      get().removeNotification(id);
    }, 5000);
  },

  removeNotification: (id) => {
    set((state) => ({
      notifications: state.notifications.filter(n => n.id !== id)
    }));
  },

  clearAll: () => set({ notifications: [] }),
}));

// 4. Clean component usage
function Sidebar() {
  const { user, logout } = useUserStore();
  const { theme } = useThemeStore();

  return (
    <aside className={`sidebar ${theme}`}>
      <UserProfile user={user} onLogout={logout} />
    </aside>
  );
}

function NotificationPanel() {
  const { notifications, removeNotification } = useNotificationStore();

  return (
    <div className="notifications">
      {notifications.map(notification => (
        <Notification
          key={notification.id}
          {...notification}
          onClose={() => removeNotification(notification.id)}
        />
      ))}
    </div>
  );
}

2. Performance Nightmare: Unnecessary Re-renders

The Problem: Everything Re-renders on Every State Change

// ❌ PERFORMANCE DISASTER
function App() {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  const [comments, setComments] = useState([]);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);

  // Every state change re-renders ALL components
  return (
    <div>
      <Header user={user} theme={theme} notifications={notifications} />
      <PostList posts={posts} />
      <CommentList comments={comments} />
      <Sidebar user={user} />
    </div>
  );
}

// When theme changes, PostList and CommentList unnecessarily re-render
// When new comment is added, Header and Sidebar re-render

Solution 1: React.memo and Proper Memoization

// ✅ OPTIMIZED WITH MEMOIZATION
import React, { memo, useMemo, useCallback } from 'react';

// 1. Memoize expensive components
const PostList = memo(function PostList({ posts, onPostClick }) {
  console.log('PostList rendering'); // This should only log when posts change

  const sortedPosts = useMemo(() => {
    return posts.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
  }, [posts]);

  return (
    <div className="post-list">
      {sortedPosts.map(post => (
        <PostItem
          key={post.id}
          post={post}
          onClick={onPostClick}
        />
      ))}
    </div>
  );
});

// 2. Memoize post items to prevent cascade re-renders
const PostItem = memo(function PostItem({ post, onClick }) {
  const handleClick = useCallback(() => {
    onClick(post.id);
  }, [post.id, onClick]);

  return (
    <article className="post" onClick={handleClick}>
      <h3>{post.title}</h3>
      <p>{post.excerpt}</p>
    </article>
  );
});

// 3. Split context to prevent unnecessary updates
const ThemeContext = createContext();
const UserContext = createContext();
const PostsContext = createContext();

function App() {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');

  return (
    <UserContext.Provider value={{ user, setUser }}>
      <ThemeContext.Provider value={{ theme, setTheme }}>
        <PostsProvider>
          <Layout />
        </PostsProvider>
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

function PostsProvider({ children }) {
  const [posts, setPosts] = useState([]);
  const [comments, setComments] = useState([]);

  const addComment = useCallback((postId, comment) => {
    setComments(prev => [...prev, { ...comment, postId }]);
  }, []);

  return (
    <PostsContext.Provider value={{ posts, setPosts, comments, addComment }}>
      {children}
    </PostsContext.Provider>
  );
}

Solution 2: Selector-Based State Management

// ✅ EFFICIENT SELECTOR PATTERN WITH ZUSTAND
import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';

const useAppStore = create(
  subscribeWithSelector((set, get) => ({
    // State
    user: null,
    posts: [],
    comments: [],
    theme: 'light',

    // Actions
    setUser: (user) => set({ user }),
    addPost: (post) => set((state) => ({ posts: [...state.posts, post] })),
    addComment: (comment) => set((state) => ({ comments: [...state.comments, comment] })),
    setTheme: (theme) => set({ theme }),
  }))
);

// 1. Theme-aware components only re-render on theme changes
function Header() {
  const theme = useAppStore((state) => state.theme);
  const user = useAppStore((state) => state.user);

  return (
    <header className={`header ${theme}`}>
      <UserProfile user={user} />
    </header>
  );
}

// 2. Post components only re-render when posts change
function PostList() {
  const posts = useAppStore((state) => state.posts);

  return (
    <div className="post-list">
      {posts.map(post => (
        <PostItem key={post.id} postId={post.id} />
      ))}
    </div>
  );
}

// 3. Individual post items only re-render when their specific post changes
function PostItem({ postId }) {
  const post = useAppStore((state) => 
    state.posts.find(p => p.id === postId)
  );

  if (!post) return null;

  return (
    <article className="post">
      <h3>{post.title}</h3>
      <p>{post.content}</p>
    </article>
  );
}

// 4. Comments only re-render when comments for specific post change
function CommentList({ postId }) {
  const comments = useAppStore((state) =>
    state.comments.filter(c => c.postId === postId)
  );

  return (
    <div className="comments">
      {comments.map(comment => (
        <CommentItem key={comment.id} comment={comment} />
      ))}
    </div>
  );
}

3. Stale Closure Hell

The Problem: Outdated State in Callbacks

// ❌ STALE CLOSURE NIGHTMARE
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      // BUG: This always logs 0, never the current count
      console.log('Current count:', count);

      // BUG: This increments from 0 every time
      setCount(count + 1);
    }, 1000);

    return () => clearInterval(interval);
  }, []); // Empty dependency array creates stale closure

  return <div>Count: {count}</div>;
}

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const socket = new WebSocket(`ws://localhost:8080/room/${roomId}`);

    socket.onmessage = (event) => {
      const message = JSON.parse(event.data);
      // BUG: messages is always the initial empty array
      setMessages(messages.concat(message));
    };

    return () => socket.close();
  }, [roomId]); // Missing messages dependency

  return (
    <div>
      {messages.map(msg => (
        <div key={msg.id}>{msg.text}</div>
      ))}
    </div>
  );
}

Solution 1: Functional Updates and Refs

// ✅ FUNCTIONAL UPDATES SOLVE STALE CLOSURES
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      // ✅ Use functional update to access current state
      setCount(currentCount => {
        console.log('Current count:', currentCount);
        return currentCount + 1;
      });
    }, 1000);

    return () => clearInterval(interval);
  }, []); // Empty array is fine with functional updates

  return <div>Count: {count}</div>;
}

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const socket = new WebSocket(`ws://localhost:8080/room/${roomId}`);

    socket.onmessage = (event) => {
      const message = JSON.parse(event.data);
      // ✅ Functional update accesses current messages
      setMessages(currentMessages => [...currentMessages, message]);
    };

    return () => socket.close();
  }, [roomId]); // Only roomId needed

  return (
    <div>
      {messages.map(msg => (
        <div key={msg.id}>{msg.text}</div>
      ))}
    </div>
  );
}

Solution 2: useRef for Latest Value Access

// ✅ USING REFS FOR ALWAYS-CURRENT VALUES
function useLatest(value) {
  const ref = useRef(value);
  ref.current = value;
  return ref;
}

function Timer() {
  const [count, setCount] = useState(0);
  const [delay, setDelay] = useState(1000);
  const countRef = useLatest(count);
  const delayRef = useLatest(delay);

  useEffect(() => {
    const interval = setInterval(() => {
      // ✅ Always get the current values
      console.log('Current count:', countRef.current);
      console.log('Current delay:', delayRef.current);

      setCount(c => c + 1);
    }, delayRef.current);

    return () => clearInterval(interval);
  }, []); // Empty dependency array is safe

  return (
    <div>
      <div>Count: {count}</div>
      <button onClick={() => setDelay(d => d / 2)}>
        Speed up (current delay: {delay}ms)
      </button>
    </div>
  );
}

function WebSocketChat({ roomId, userId }) {
  const [messages, setMessages] = useState([]);
  const [connectionStatus, setConnectionStatus] = useState('connecting');
  const userIdRef = useLatest(userId);

  useEffect(() => {
    const socket = new WebSocket(`ws://localhost:8080/room/${roomId}`);

    socket.onopen = () => {
      setConnectionStatus('connected');
      // ✅ Send current user ID when connecting
      socket.send(JSON.stringify({
        type: 'join',
        userId: userIdRef.current
      }));
    };

    socket.onmessage = (event) => {
      const message = JSON.parse(event.data);
      setMessages(msgs => [...msgs, message]);
    };

    socket.onclose = () => {
      setConnectionStatus('disconnected');
    };

    return () => {
      // ✅ Access current user ID for cleanup
      socket.send(JSON.stringify({
        type: 'leave',
        userId: userIdRef.current
      }));
      socket.close();
    };
  }, [roomId]); // Only roomId in dependencies

  return (
    <div>
      <div>Status: {connectionStatus}</div>
      {messages.map(msg => (
        <div key={msg.id}>{msg.text}</div>
      ))}
    </div>
  );
}

4. Async State Management Chaos

The Problem: Loading States and Race Conditions

// ❌ ASYNC STATE CHAOS
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);

    // BUG: Race condition - if userId changes quickly,
    // older requests might complete after newer ones
    fetchUser(userId)
      .then(userData => {
        setUser(userData);
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, [userId]);

  // BUG: No cleanup, no error state reset

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  return <div>User: {user?.name}</div>;
}

Solution 1: Custom Async Hook with Cleanup

// ✅ ROBUST ASYNC STATE MANAGEMENT
function useAsyncState(asyncFunction, dependencies = [], initialData = null) {
  const [state, setState] = useState({
    data: initialData,
    loading: false,
    error: null
  });

  const abortControllerRef = useRef();

  useEffect(() => {
    let cancelled = false;

    // Cancel previous request
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }

    // Create new abort controller
    abortControllerRef.current = new AbortController();
    const signal = abortControllerRef.current.signal;

    setState(prev => ({ ...prev, loading: true, error: null }));

    asyncFunction(signal)
      .then(data => {
        if (!cancelled && !signal.aborted) {
          setState({ data, loading: false, error: null });
        }
      })
      .catch(error => {
        if (!cancelled && !signal.aborted) {
          setState(prev => ({ 
            ...prev, 
            loading: false, 
            error: error.message 
          }));
        }
      });

    return () => {
      cancelled = true;
      if (abortControllerRef.current) {
        abortControllerRef.current.abort();
      }
    };
  }, dependencies);

  const retry = useCallback(() => {
    setState(prev => ({ ...prev, error: null }));
    // Trigger useEffect by updating a dependency
  }, []);

  return { ...state, retry };
}

// Usage
function UserProfile({ userId }) {
  const { data: user, loading, error, retry } = useAsyncState(
    (signal) => fetchUser(userId, { signal }),
    [userId]
  );

  if (loading) return <div>Loading user...</div>;
  if (error) return (
    <div>
      Error: {error}
      <button onClick={retry}>Retry</button>
    </div>
  );

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

Solution 2: React Query for Server State

// ✅ REACT QUERY - THE ASYNC STATE SOLUTION
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// 1. User profile with caching and background updates
function UserProfile({ userId }) {
  const {
    data: user,
    isLoading,
    error,
    refetch,
    isStale
  } = useQuery({
    queryKey: ['user', userId],
    queryFn: ({ signal }) => fetchUser(userId, { signal }),
    staleTime: 5 * 60 * 1000, // 5 minutes
    cacheTime: 10 * 60 * 1000, // 10 minutes
    retry: 3,
    retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
  });

  if (isLoading) return <div>Loading user...</div>;
  if (error) return (
    <div>
      Error: {error.message}
      <button onClick={() => refetch()}>Retry</button>
    </div>
  );

  return (
    <div>
      <h2>{user.name} {isStale && '(updating...)'}</h2>
      <p>{user.email}</p>
      <EditUserForm user={user} />
    </div>
  );
}

// 2. Optimistic updates with mutations
function EditUserForm({ user }) {
  const queryClient = useQueryClient();
  const [formData, setFormData] = useState({
    name: user.name,
    email: user.email
  });

  const updateUserMutation = useMutation({
    mutationFn: (userData) => updateUser(user.id, userData),

    // Optimistic update
    onMutate: async (newUserData) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries(['user', user.id]);

      // Snapshot previous value
      const previousUser = queryClient.getQueryData(['user', user.id]);

      // Optimistically update
      queryClient.setQueryData(['user', user.id], old => ({
        ...old,
        ...newUserData
      }));

      return { previousUser };
    },

    // On error, rollback
    onError: (err, newUserData, context) => {
      queryClient.setQueryData(['user', user.id], context.previousUser);
    },

    // Always refetch after error or success
    onSettled: () => {
      queryClient.invalidateQueries(['user', user.id]);
    },
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    updateUserMutation.mutate(formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={formData.name}
        onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
        disabled={updateUserMutation.isLoading}
      />
      <input
        value={formData.email}
        onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
        disabled={updateUserMutation.isLoading}
      />
      <button type="submit" disabled={updateUserMutation.isLoading}>
        {updateUserMutation.isLoading ? 'Saving...' : 'Save'}
      </button>
      {updateUserMutation.error && (
        <div className="error">
          Error: {updateUserMutation.error.message}
        </div>
      )}
    </form>
  );
}

// 3. Infinite scroll with React Query
function PostList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isLoading,
    error
  } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: ({ pageParam = 0 }) => fetchPosts({ page: pageParam }),
    getNextPageParam: (lastPage, pages) => lastPage.nextPage ?? undefined,
  });

  const posts = data?.pages.flatMap(page => page.posts) ?? [];

  if (isLoading) return <div>Loading posts...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      {posts.map(post => (
        <PostItem key={post.id} post={post} />
      ))}

      {hasNextPage && (
        <button
          onClick={() => fetchNextPage()}
          disabled={isFetchingNextPage}
        >
          {isFetchingNextPage ? 'Loading more...' : 'Load More'}
        </button>
      )}
    </div>
  );
}

5. Form State Management Hell

The Problem: Complex Form Logic

// ❌ FORM STATE NIGHTMARE
function UserRegistrationForm() {
  const [formData, setFormData] = useState({
    email: '',
    password: '',
    confirmPassword: '',
    firstName: '',
    lastName: '',
    acceptTerms: false
  });
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  // Massive validation logic
  const validateField = (name, value) => {
    let error = '';

    switch (name) {
      case 'email':
        if (!value) error = 'Email is required';
        else if (!/\S+@\S+\.\S+/.test(value)) error = 'Email is invalid';
        break;
      case 'password':
        if (!value) error = 'Password is required';
        else if (value.length < 8) error = 'Password must be at least 8 characters';
        break;
      case 'confirmPassword':
        if (!value) error = 'Confirm password is required';
        else if (value !== formData.password) error = 'Passwords do not match';
        break;
      // ... more validation
    }

    setErrors(prev => ({ ...prev, [name]: error }));
  };

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    const fieldValue = type === 'checkbox' ? checked : value;

    setFormData(prev => ({ ...prev, [name]: fieldValue }));
    validateField(name, fieldValue);
  };

  const handleBlur = (e) => {
    const { name } = e.target;
    setTouched(prev => ({ ...prev, [name]: true }));
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);

    // Validate all fields
    Object.keys(formData).forEach(key => {
      validateField(key, formData[key]);
    });

    // Check if form is valid
    if (Object.values(errors).some(error => error)) {
      setIsSubmitting(false);
      return;
    }

    try {
      await submitRegistration(formData);
    } catch (error) {
      // Handle error
    } finally {
      setIsSubmitting(false);
    }
  };

  // Massive JSX with repeated error handling...
}

Solution: React Hook Form with Validation

// ✅ REACT HOOK FORM - CLEAN AND PERFORMANT
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

// 1. Schema-based validation
const registrationSchema = z.object({
  email: z.string().email('Invalid email address'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
  confirmPassword: z.string(),
  firstName: z.string().min(1, 'First name is required'),
  lastName: z.string().min(1, 'Last name is required'),
  acceptTerms: z.boolean().refine(val => val === true, 'You must accept the terms')
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ["confirmPassword"],
});

function UserRegistrationForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting, isValid },
    watch,
    reset,
    setError
  } = useForm({
    resolver: zodResolver(registrationSchema),
    mode: 'onBlur',
    defaultValues: {
      email: '',
      password: '',
      confirmPassword: '',
      firstName: '',
      lastName: '',
      acceptTerms: false
    }
  });

  const onSubmit = async (data) => {
    try {
      await submitRegistration(data);
      reset(); // Clear form on success
    } catch (error) {
      // Handle server errors
      if (error.code === 'EMAIL_EXISTS') {
        setError('email', { 
          type: 'server', 
          message: 'Email already exists' 
        });
      } else {
        setError('root', { 
          type: 'server', 
          message: error.message 
        });
      }
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="registration-form">
      <div className="form-group">
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="email"
          {...register('email')}
          className={errors.email ? 'error' : ''}
        />
        {errors.email && (
          <span className="error-message">{errors.email.message}</span>
        )}
      </div>

      <div className="form-group">
        <label htmlFor="password">Password</label>
        <input
          id="password"
          type="password"
          {...register('password')}
          className={errors.password ? 'error' : ''}
        />
        {errors.password && (
          <span className="error-message">{errors.password.message}</span>
        )}
      </div>

      <div className="form-group">
        <label htmlFor="confirmPassword">Confirm Password</label>
        <input
          id="confirmPassword"
          type="password"
          {...register('confirmPassword')}
          className={errors.confirmPassword ? 'error' : ''}
        />
        {errors.confirmPassword && (
          <span className="error-message">{errors.confirmPassword.message}</span>
        )}
      </div>

      <div className="form-group">
        <label htmlFor="firstName">First Name</label>
        <input
          id="firstName"
          type="text"
          {...register('firstName')}
          className={errors.firstName ? 'error' : ''}
        />
        {errors.firstName && (
          <span className="error-message">{errors.firstName.message}</span>
        )}
      </div>

      <div className="form-group">
        <label htmlFor="lastName">Last Name</label>
        <input
          id="lastName"
          type="text"
          {...register('lastName')}
          className={errors.lastName ? 'error' : ''}
        />
        {errors.lastName && (
          <span className="error-message">{errors.lastName.message}</span>
        )}
      </div>

      <div className="form-group">
        <label>
          <input
            type="checkbox"
            {...register('acceptTerms')}
          />
          I accept the terms and conditions
        </label>
        {errors.acceptTerms && (
          <span className="error-message">{errors.acceptTerms.message}</span>
        )}
      </div>

      {errors.root && (
        <div className="error-message">{errors.root.message}</div>
      )}

      <button
        type="submit"
        disabled={!isValid || isSubmitting}
        className="submit-button"
      >
        {isSubmitting ? 'Creating Account...' : 'Create Account'}
      </button>
    </form>
  );
}

// 2. Custom form field component
function FormField({ label, error, children }) {
  return (
    <div className="form-group">
      <label>{label}</label>
      {children}
      {error && <span className="error-message">{error.message}</span>}
    </div>
  );
}

// 3. Simplified form with custom components
function SimpleRegistrationForm() {
  const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm({
    resolver: zodResolver(registrationSchema)
  });

  const onSubmit = async (data) => {
    await submitRegistration(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <FormField label="Email" error={errors.email}>
        <input type="email" {...register('email')} />
      </FormField>

      <FormField label="Password" error={errors.password}>
        <input type="password" {...register('password')} />
      </FormField>

      <FormField label="Confirm Password" error={errors.confirmPassword}>
        <input type="password" {...register('confirmPassword')} />
      </FormField>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Creating...' : 'Create Account'}
      </button>
    </form>
  );
}

6. Modern State Management Patterns (2024-2025)

Pattern 1: Server State vs Client State Separation

// ✅ CLEAR SEPARATION OF CONCERNS
import { create } from 'zustand';
import { useQuery } from '@tanstack/react-query';

// Client state (UI state, user preferences)
const useUIStore = create((set) => ({
  theme: 'light',
  sidebarOpen: false,
  notifications: [],

  toggleTheme: () => set((state) => ({ 
    theme: state.theme === 'light' ? 'dark' : 'light' 
  })),
  toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
  addNotification: (notification) => set((state) => ({
    notifications: [...state.notifications, notification]
  })),
}));

// Server state (user data, posts, comments) - managed by React Query
function useUser(userId) {
  return useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    staleTime: 5 * 60 * 1000,
  });
}

function usePosts() {
  return useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    staleTime: 2 * 60 * 1000,
  });
}

// Component using both types of state
function Dashboard({ userId }) {
  const { theme, sidebarOpen, toggleSidebar } = useUIStore();
  const { data: user, isLoading: userLoading } = useUser(userId);
  const { data: posts, isLoading: postsLoading } = usePosts();

  if (userLoading || postsLoading) return <div>Loading...</div>;

  return (
    <div className={`dashboard ${theme}`}>
      <Sidebar isOpen={sidebarOpen} onToggle={toggleSidebar} />
      <MainContent user={user} posts={posts} />
    </div>
  );
}

Pattern 2: Feature-Based State Architecture

// ✅ FEATURE-BASED STATE ORGANIZATION
// features/auth/store.js
export const useAuthStore = create((set, get) => ({
  user: null,
  isAuthenticated: false,

  login: async (credentials) => {
    const user = await authAPI.login(credentials);
    set({ user, isAuthenticated: true });
  },

  logout: () => {
    authAPI.logout();
    set({ user: null, isAuthenticated: false });
  },
}));

// features/posts/store.js
export const usePostsStore = create((set, get) => ({
  selectedPostId: null,
  filters: { category: 'all', sortBy: 'date' },

  selectPost: (postId) => set({ selectedPostId: postId }),
  setFilter: (key, value) => set((state) => ({
    filters: { ...state.filters, [key]: value }
  })),
}));

// features/posts/hooks.js
export function usePosts() {
  const { filters } = usePostsStore();

  return useQuery({
    queryKey: ['posts', filters],
    queryFn: () => fetchPosts(filters),
  });
}

export function useSelectedPost() {
  const { selectedPostId } = usePostsStore();

  return useQuery({
    queryKey: ['post', selectedPostId],
    queryFn: () => fetchPost(selectedPostId),
    enabled: !!selectedPostId,
  });
}

// App structure
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Router>
        <Routes>
          <Route path="/posts" element={<PostsPage />} />
          <Route path="/post/:id" element={<PostPage />} />
        </Routes>
      </Router>
    </QueryClientProvider>
  );
}

7. Best Practices Summary

State Management Checklist ✅

Architecture Decisions

  • [ ] Separate server state (React Query) from client state (Zustand/Context)
  • [ ] Use feature-based state organization
  • [ ] Implement proper loading and error states
  • [ ] Plan for offline scenarios and optimistic updates

Performance Optimization

  • [ ] Memoize expensive components with React.memo
  • [ ] Use selector patterns to prevent unnecessary re-renders
  • [ ] Implement proper dependency arrays in useEffect
  • [ ] Avoid prop drilling with strategic context usage

Developer Experience

  • [ ] Use TypeScript for type safety
  • [ ] Implement proper error boundaries
  • [ ] Add dev tools for debugging (Redux DevTools, React Query DevTools)
  • [ ] Write comprehensive tests for state logic

Common Pitfalls to Avoid

  • [ ] Don't put everything in global state
  • [ ] Avoid massive context providers that cause cascade re-renders
  • [ ] Don't ignore the stale closure problem
  • [ ] Never mutate state directly
  • [ ] Avoid unnecessary useEffect dependencies

React state management in 2024-2025 requires a thoughtful approach that balances performance, developer experience, and maintainability. Choose your tools wisely and always consider the complexity vs. benefit trade-off.