
ReactJS
As applications grow, naive state management creates performance problems and architectural complexity. Advanced patterns solve these challenges through careful design, memoization, and strategic state placement.
Deeply nested state structures cause unnecessary re-renders. Normalize state to keep related data flat, then denormalize only when needed for display.
State shape determines re-render behavior. Nested structures mean updating deep properties causes entire parent objects to change, triggering cascading re-renders. Normalization keeps related data together while avoiding nesting.
import { useState, useMemo } from 'react';
// Before: Deeply nested structure
interface BadUserState {
users: {
[userId: string]: {
profile: {
name: string;
email: string;
preferences: {
theme: 'light' | 'dark';
notifications: boolean;
};
};
posts: Array<{ id: string; title: string }>;
};
};
}
// After: Normalized state
interface GoodUserState {
users: Record<string, { id: string; name: string; email: string }>;
userPreferences: Record<string, { theme: 'light' | 'dark'; notifications: boolean }>;
userPosts: Record<string, string[]>; // userId -> postIds
posts: Record<string, { id: string; title: string }>;
}
function UserManagement() {
const [state, setState] = useState<GoodUserState>({
users: {
'user-1': { id: 'user-1', name: 'John', email: 'john@example.com' },
},
userPreferences: {
'user-1': { theme: 'dark', notifications: true },
},
userPosts: {
'user-1': ['post-1', 'post-2'],
},
posts: {
'post-1': { id: 'post-1', title: 'First Post' },
'post-2': { id: 'post-2', title: 'Second Post' },
},
});
// Denormalize on demand for display
const userWithPosts = useMemo(() => {
const userId = 'user-1';
const user = state.users[userId];
const postIds = state.userPosts[userId] || [];
return {
...user,
posts: postIds.map(id => state.posts[id]),
};
}, [state.users, state.userPosts, state.posts]);
const updateUserPreference = (userId: string, theme: 'light' | 'dark') => {
setState(prev => ({
...prev,
userPreferences: {
...prev.userPreferences,
[userId]: {
...prev.userPreferences[userId],
theme,
},
},
}));
};
return (
<div>
<p>User: {userWithPosts.name}</p>
<p>Posts: {userWithPosts.posts.length}</p>
</div>
);
}<details>
<summary>📚 More Examples</summary>
// Example 2: Selectors for consistent denormalization
interface AppState {
users: Record<string, { id: string; name: string }>;
posts: Record<string, { id: string; title: string; authorId: string }>;
comments: Record<string, { id: string; text: string; postId: string }>;
}
// Selector functions encapsulate denormalization logic
const selectors = {
getPost: (state: AppState, postId: string) => {
const post = state.posts[postId];
return {
...post,
author: state.users[post.authorId],
comments: Object.values(state.comments).filter(
comment => comment.postId === postId
),
};
},
getUser: (state: AppState, userId: string) => {
return {
...state.users[userId],
posts: Object.values(state.posts).filter(
post => post.authorId === userId
),
};
},
};
function Post({ postId, state }: { postId: string; state: AppState }) {
const post = selectors.getPost(state, postId);
return (
<div>
<h2>{post.title}</h2>
<p>By {post.author.name}</p>
<ul>
{post.comments.map(comment => (
<li key={comment.id}>{comment.text}</li>
))}
</ul>
</div>
);
}</details>
When multiple state values always change together or depend on each other, use compound state with a reducer for clarity and correctness.
Compound state uses `useReducer` instead of multiple `useState` calls. This centralizes state logic, prevents inconsistent states, and makes complex state transitions explicit.
import { useReducer } from 'react';
interface FormState {
values: { email: string; password: string; rememberMe: boolean };
touched: { email: boolean; password: boolean };
errors: { email?: string; password?: string };
isSubmitting: boolean;
}
type FormAction =
| { type: 'SET_FIELD'; field: string; value: string | boolean }
| { type: 'SET_TOUCHED'; field: string }
| { type: 'SET_ERROR'; field: string; error: string }
| { type: 'CLEAR_ERROR'; field: string }
| { type: 'START_SUBMIT' }
| { type: 'END_SUBMIT' }
| { type: 'RESET' };
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case 'SET_FIELD':
return {
...state,
values: { ...state.values, [action.field]: action.value },
};
case 'SET_TOUCHED':
return {
...state,
touched: { ...state.touched, [action.field]: true },
};
case 'SET_ERROR':
return {
...state,
errors: { ...state.errors, [action.field]: action.error },
};
case 'CLEAR_ERROR':
return {
...state,
errors: { ...state.errors, [action.field]: undefined },
};
case 'START_SUBMIT':
return { ...state, isSubmitting: true };
case 'END_SUBMIT':
return { ...state, isSubmitting: false };
case 'RESET':
return {
values: { email: '', password: '', rememberMe: false },
touched: {},
errors: {},
isSubmitting: false,
};
default:
return state;
}
}
function LoginForm() {
const [state, dispatch] = useReducer(formReducer, {
values: { email: '', password: '', rememberMe: false },
touched: {},
errors: {},
isSubmitting: false,
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
dispatch({ type: 'START_SUBMIT' });
try {
// Validate
if (!state.values.email.includes('@')) {
dispatch({ type: 'SET_ERROR', field: 'email', error: 'Invalid email' });
return;
}
// Submit
await new Promise(resolve => setTimeout(resolve, 1000));
dispatch({ type: 'RESET' });
} finally {
dispatch({ type: 'END_SUBMIT' });
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={state.values.email}
onChange={(e) =>
dispatch({ type: 'SET_FIELD', field: 'email', value: e.target.value })
}
/>
{state.errors.email && <p style={{ color: 'red' }}>{state.errors.email}</p>}
</form>
);
}<details>
<summary>📚 More Examples</summary>
// Example 2: State machine with useReducer
type PaymentState = 'idle' | 'processing' | 'success' | 'error';
interface PaymentReducerState {
state: PaymentState;
amount: number;
error: string | null;
}
type PaymentAction =
| { type: 'START'; amount: number }
| { type: 'SUCCESS' }
| { type: 'ERROR'; message: string }
| { type: 'RESET' };
const paymentReducer = (
state: PaymentReducerState,
action: PaymentAction
): PaymentReducerState => {
switch (action.type) {
case 'START':
return {
...state,
state: 'processing',
amount: action.amount,
error: null,
};
case 'SUCCESS':
return { ...state, state: 'success' };
case 'ERROR':
return { ...state, state: 'error', error: action.message };
case 'RESET':
return {
state: 'idle',
amount: 0,
error: null,
};
}
};
function PaymentProcessor() {
const [payment, dispatch] = useReducer(paymentReducer, {
state: 'idle',
amount: 0,
error: null,
});
const processPayment = async (amount: number) => {
dispatch({ type: 'START', amount });
try {
await new Promise(resolve => setTimeout(resolve, 2000));
dispatch({ type: 'SUCCESS' });
} catch (error) {
dispatch({
type: 'ERROR',
message: error instanceof Error ? error.message : 'Payment failed',
});
}
};
return (
<div>
{payment.state === 'idle' && (
<button onClick={() => processPayment(99.99)}>Pay $99.99</button>
)}
{payment.state === 'processing' && <p>Processing...</p>}
{payment.state === 'success' && <p>Payment successful!</p>}
{payment.state === 'error' && <p>Error: {payment.error}</p>}
</div>
);
}</details>
Divide state into small, independent atoms. Each piece changes for one reason and doesn't depend on others, enabling fine-grained updates.
Atomic state prevents the "tearing" problem where different components see inconsistent state during render. Each atom updates independently, components subscribe to relevant atoms only.
import { useState, useCallback } from 'react';
// Atomic state pattern
function useAtomicState<T>(initialValue: T) {
const [value, setValue] = useState(initialValue);
return [value, setValue] as const;
}
function App() {
// Each piece of state is atomic
const [userId, setUserId] = useAtomicState<string | null>(null);
const [posts, setPosts] = useAtomicState<Array<{ id: string; title: string }>>([]);
const [loading, setLoading] = useAtomicState(false);
const [error, setError] = useAtomicState<string | null>(null);
// Queries select only what they need
const selectedUserId = userId;
const postCount = posts.length;
const isLoading = loading;
const fetchPosts = useCallback(
async (id: string) => {
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/users/${id}/posts`);
setPosts(await response.json());
} catch (err) {
setError(err instanceof Error ? err.message : 'Error');
} finally {
setLoading(false);
}
},
[setLoading, setError, setPosts]
);
return (
<div>
<p>User: {selectedUserId}</p>
<p>Posts: {postCount}</p>
{isLoading && <p>Loading...</p>}
{error && <p>Error: {error}</p>}
</div>
);
}┌─────────────────────┬──────────────────┬─────────────────┐
│ Pattern │ Best For │ Trade-offs │
├─────────────────────┼──────────────────┼─────────────────┤
│ Atomic │ Simple apps │ Many useState │
│ Compound │ Related data │ Complex reducer │
│ Normalized │ Complex data │ Denormalization │
│ State machine │ Complex flows │ Verbose actions │
└─────────────────────┴──────────────────┴─────────────────┘Ready to practice? Challenges | Next: useState Hooks Deep Dive
Resources
Ojasa Mirai
Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.
Learn Deep • Build Real • Verify Skills • Launch Forward