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
- Prop Drilling Hell: Passing props through 10+ components
- Performance Nightmares: Unnecessary re-renders crushing UX
- Stale Closure Traps: useEffect and event handlers with outdated state
- Async State Chaos: Loading states, race conditions, and data inconsistency
- Context Overuse: React Context becoming a performance bottleneck
- 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.