
ReactJS
State changes trigger re-renders. Poor state management causes cascading re-renders, tanking performance. Master optimization techniques to keep applications fast.
Every state change triggers a re-render. But unnecessary re-renders happen when:
1. A parent re-renders, triggering all children
2. Context value changes, causing all consumers to re-render
3. Props change even if not used in the component
4. Object/array references change, breaking memoization
import { useState, memo, useMemo } from 'react';
// β BAD: Unnecessary re-renders cascade
function BadParent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<input value={name} onChange={(e) => setName(e.target.value)} />
{/* Both children re-render when parent re-renders, even if their props didn't change */}
<ExpensiveChildA count={count} />
<ExpensiveChildB name={name} />
</div>
);
}
function ExpensiveChildA({ count }: { count: number }) {
// Very expensive computation
let sum = 0;
for (let i = 0; i < 100000000; i++) {
sum += i;
}
return <div>Count: {count}, Sum: {sum}</div>;
}
function ExpensiveChildB({ name }: { name: string }) {
let sum = 0;
for (let i = 0; i < 100000000; i++) {
sum += i;
}
return <div>Name: {name}, Sum: {sum}</div>;
}
// β
GOOD: Memoize children to prevent unnecessary re-renders
const MemoizedChildA = memo(ExpensiveChildA);
const MemoizedChildB = memo(ExpensiveChildB);
function GoodParent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<input value={name} onChange={(e) => setName(e.target.value)} />
{/* Only re-render when their specific props change */}
<MemoizedChildA count={count} />
<MemoizedChildB name={name} />
</div>
);
}<details>
<summary>π More Examples</summary>
// Example: Identifying expensive components with Profiler
import { Profiler, ProfilerOnRenderCallback } from 'react';
const onRenderCallback: ProfilerOnRenderCallback = (
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) => {
console.log(`${id} (${phase}) took ${actualDuration}ms`);
};
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Parent />
</Profiler>
);
}
// Example: Memoizing with dependencies
interface DataListProps {
items: string[];
onSelect: (item: string) => void;
}
function DataList({ items, onSelect }: DataListProps) {
const sorted = useMemo(() => {
console.log('Sorting items...');
return [...items].sort();
}, [items]); // Only sort when items change
return (
<ul>
{sorted.map((item) => (
<li key={item}>
<button onClick={() => onSelect(item)}>{item}</button>
</li>
))}
</ul>
);
}</details>
Split state into multiple useState calls so unrelated changes don't trigger unnecessary re-renders. This is especially important in context providers.
Related state updates together. Unrelated state in separate calls. This enables fine-grained subscriptions.
import { useState, memo, useCallback } from 'react';
// β BAD: Single state object
function BadStateComponent() {
const [state, setState] = useState({
user: null,
theme: 'light',
notifications: [],
sidebarOpen: false,
});
const updateTheme = () => {
setState((prev) => ({ ...prev, theme: prev.theme === 'light' ? 'dark' : 'light' }));
};
const toggleSidebar = () => {
setState((prev) => ({ ...prev, sidebarOpen: !prev.sidebarOpen }));
};
// Changing theme also re-renders SidebarComponent even though it doesn't use theme
return (
<div>
<button onClick={updateTheme}>Toggle Theme</button>
<SidebarComponent isOpen={state.sidebarOpen} toggle={toggleSidebar} />
</div>
);
}
// β
GOOD: Split state by concern
function GoodStateComponent() {
const [theme, setTheme] = useState('light');
const [sidebarOpen, setSidebarOpen] = useState(false);
const updateTheme = useCallback(
() => setTheme((prev) => (prev === 'light' ? 'dark' : 'light')),
[]
);
const toggleSidebar = useCallback(() => setSidebarOpen((prev) => !prev), []);
// SidebarComponent only re-renders when sidebarOpen changes
return (
<div>
<button onClick={updateTheme}>Toggle Theme</button>
<MemoizedSidebar isOpen={sidebarOpen} toggle={toggleSidebar} />
</div>
);
}
const MemoizedSidebar = memo(function SidebarComponent({
isOpen,
toggle,
}: {
isOpen: boolean;
toggle: () => void;
}) {
return (
<aside>
<button onClick={toggle}>{isOpen ? 'Close' : 'Open'}</button>
{isOpen && <nav>Navigation menu</nav>}
</aside>
);
});<details>
<summary>π More Examples</summary>
// Example: State splitting in list rendering
interface TodoItem {
id: string;
text: string;
completed: boolean;
}
// β BAD: Updating one todo re-renders entire list
function BadTodoList() {
const [todos, setTodos] = useState<TodoItem[]>([]);
const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');
const [editingId, setEditingId] = useState<string | null>(null);
return (
<div>
<select value={filter} onChange={(e) => setFilter(e.target.value as any)}>
<option value="all">All</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
</select>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
isEditing={editingId === todo.id}
onToggleEdit={(id) => setEditingId(id === editingId ? null : id)}
/>
))}
</div>
);
}
// β
GOOD: Separate state by concern
function GoodTodoList() {
const [todos, setTodos] = useState<TodoItem[]>([]);
const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');
const [editingIds, setEditingIds] = useState<Set<string>>(new Set());
const filteredTodos = todos.filter((todo) => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
});
return (
<div>
<select value={filter} onChange={(e) => setFilter(e.target.value as any)}>
<option value="all">All</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
</select>
{filteredTodos.map((todo) => (
<MemoTodoItem
key={todo.id}
todo={todo}
isEditing={editingIds.has(todo.id)}
onToggleEdit={(id) => {
const newIds = new Set(editingIds);
if (newIds.has(id)) {
newIds.delete(id);
} else {
newIds.add(id);
}
setEditingIds(newIds);
}}
/>
))}
</div>
);
}
const MemoTodoItem = memo(function TodoItem({
todo,
isEditing,
onToggleEdit,
}: any) {
return <li>{todo.text}</li>;
});</details>
useCallback prevents new function references from breaking memoization. useReducer can optimize state updates with many dependencies.
Callbacks and state updaters should be stable references. useCallback memoizes functions.
import { useState, useCallback, useReducer } from 'react';
// β BAD: New callback on every render breaks memo
function BadParent() {
const [count, setCount] = useState(0);
// New function every render!
const handleIncrement = () => {
setCount((prev) => prev + 1);
};
return <MemoChild onIncrement={handleIncrement} />;
}
const MemoChild = memo(function Child({
onIncrement,
}: {
onIncrement: () => void;
}) {
// Will re-render every time because onIncrement reference changed
console.log('Child rendered');
return <button onClick={onIncrement}>Increment</button>;
});
// β
GOOD: useCallback keeps function reference stable
function GoodParent() {
const [count, setCount] = useState(0);
const handleIncrement = useCallback(() => {
setCount((prev) => prev + 1);
}, []); // Empty deps = same reference always
return <MemoChild onIncrement={handleIncrement} />;
}
// β
ALSO GOOD: useReducer for multiple related actions
interface State {
count: number;
step: number;
history: number[];
}
type Action =
| { type: 'INCREMENT' }
| { type: 'DECREMENT' }
| { type: 'SET_STEP'; step: number }
| { type: 'RESET' };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'INCREMENT':
return {
...state,
count: state.count + state.step,
history: [...state.history, state.count + state.step],
};
case 'DECREMENT':
return {
...state,
count: state.count - state.step,
history: [...state.history, state.count - state.step],
};
case 'SET_STEP':
return { ...state, step: action.step };
case 'RESET':
return { count: 0, step: 1, history: [] };
default:
return state;
}
}
function OptimizedCounter() {
const [state, dispatch] = useReducer(reducer, {
count: 0,
step: 1,
history: [],
});
// Dispatch function never changes
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
<input
type="number"
value={state.step}
onChange={(e) =>
dispatch({ type: 'SET_STEP', step: parseInt(e.target.value) })
}
/>
<p>History: {state.history.join(', ')}</p>
</div>
);
}Context causes all consumers to re-render when value changes. Optimize by splitting contexts and memoizing values.
Split contexts by update frequency. Memoize context values to prevent unnecessary re-renders.
import { createContext, useContext, useState, useMemo, useCallback } from 'react';
// β BAD: Single context with all data
const BadAppContext = createContext<{
user: any;
setUser: (user: any) => void;
notifications: any[];
addNotification: (msg: string) => void;
theme: string;
toggleTheme: () => void;
} | null>(null);
// β
GOOD: Split contexts by change frequency
const UserContext = createContext<{
user: any;
setUser: (user: any) => void;
} | null>(null);
const NotificationContext = createContext<{
notifications: any[];
addNotification: (msg: string) => void;
} | null>(null);
const ThemeContext = createContext<{
theme: string;
toggleTheme: () => void;
} | null>(null);
function AppProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState(null);
const [notifications, setNotifications] = useState<any[]>([]);
const [theme, setTheme] = useState('light');
// Memoize context values to prevent unnecessary re-renders
const userValue = useMemo(() => ({ user, setUser }), [user]);
const notificationValue = useMemo(
() => ({
notifications,
addNotification: (msg: string) => {
setNotifications((prev) => [...prev, msg]);
},
}),
[notifications]
);
const themeValue = useMemo(
() => ({
theme,
toggleTheme: () => setTheme((prev) => (prev === 'light' ? 'dark' : 'light')),
}),
[theme]
);
return (
<UserContext.Provider value={userValue}>
<NotificationContext.Provider value={notificationValue}>
<ThemeContext.Provider value={themeValue}>{children}</ThemeContext.Provider>
</NotificationContext.Provider>
</UserContext.Provider>
);
}
// Only re-render when user changes
function useUser() {
return useContext(UserContext)!;
}
// Only re-render when notifications change
function useNotifications() {
return useContext(NotificationContext)!;
}
// Only re-render when theme changes
function useTheme() {
return useContext(ThemeContext)!;
}ββββββββββββββββββ¬βββββββββββββββββββ¬βββββββββββββββββ
β Technique β Impact β Complexity β
ββββββββββββββββββΌβββββββββββββββββββΌβββββββββββββββββ€
β memo() β Prevents renders β Low β
β State splittingβ Focused updates β Low-Medium β
β useCallback β Stable refs β Low β
β useMemo β Compute once β Low-Medium β
β useReducer β Complex logic β Medium β
β Context split β Granular updates β Medium β
β Profiler β Identify issues β Low β
β Code splitting β Smaller bundles β Medium-High β
ββββββββββββββββββ΄βββββββββββββββββββ΄βββββββββββββββββReady to practice? Challenges | Next: Custom Hooks for State
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