Advanced useState Patterns You Should Know
Erik Nguyen / November 7, 2024
Advanced useState Patterns You Should Know
The useState
hook is the foundation of state management in React. While it may seem simple at first, there are many advanced patterns and techniques you can use to effectively manage complex state in your applications. Let's dive in.
Complex State with Objects and Arrays
Updating Nested Objects
interface FormState {
name: string;
email: string;
address: {
street: string;
city: string;
state: string;
};
}
const EditProfileForm = () => {
const [formState, setFormState] = useState<FormState>({
name: 'John Doe',
email: 'john@example.com',
address: {
street: '123 Main St',
city: 'Anytown',
state: 'CA'
}
});
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement>
) => {
const { name, value } = e.target;
setFormState(prev => ({
...prev,
[name]: value
}));
};
const handleAddressChange = (
e: React.ChangeEvent<HTMLInputElement>
) => {
const { name, value } = e.target;
setFormState(prev => ({
...prev,
address: {
...prev.address,
[name]: value
}
}));
};
return (
<form>
<input
name="name"
value={formState.name}
onChange={handleInputChange}
/>
<input
name="email"
value={formState.email}
onChange={handleInputChange}
/>
<input
name="street"
value={formState.address.street}
onChange={handleAddressChange}
/>
{/* Other address fields */}
</form>
);
};
Use the spread operator to create new object/array references and avoid mutating the original state.
Updating Arrays
interface Task {
id: number;
text: string;
completed: boolean;
}
const TodoList = () => {
const [tasks, setTasks] = useState<Task[]>([
{ id: 1, text: 'Finish report', completed: false },
{ id: 2, text: 'Buy groceries', completed: false },
{ id: 3, text: 'Call mom', completed: true }
]);
const handleToggleTask = (id: number) => {
setTasks(prev =>
prev.map(task =>
task.id === id
? { ...task, completed: !task.completed }
: task
)
);
};
const handleDeleteTask = (id: number) => {
setTasks(prev => prev.filter(task => task.id !== id));
};
const handleAddTask = () => {
const newTask: Task = {
id: Date.now(),
text: 'New task',
completed: false
};
setTasks(prev => [...prev, newTask]);
};
return (
<div>
<button onClick={handleAddTask}>Add Task</button>
<ul>
{tasks.map(task => (
<li key={task.id}>
<input
type="checkbox"
checked={task.completed}
onChange={()=> handleToggleTask(task.id)}
/>
<span style={{ textDecoration: task.completed ? 'line-through' : 'none' }}>
{task.text}
</span>
<button onClick={()=> handleDeleteTask(task.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
};
Functional State Updates
Updating State Based on Previous State
const Counter = () => {
const [count, setCount] = useState(0);
const handleIncrement = () => {
setCount(prev => prev + 1);
};
const handleDecrement = () => {
setCount(prev => prev - 1);
};
return (
<div>
<button onClick={handleDecrement}>-</button>
<span>{count}</span>
<button onClick={handleIncrement}>+</button>
</div>
);
};
Use the functional form of setCount
when your update depends on the
previous state.
Batched State Updates
const CounterWithBatch = () => {
const [count, setCount] = useState(0);
const [otherState, setOtherState] = useState(0);
const handleClick = () => {
// React will batch these updates, only triggering one re-render
setCount(prev => prev + 1);
setOtherState(prev => prev + 1);
};
return (
<div>
<button onClick={handleClick}>Increment</button>
<span>Count: {count}</span>
<span>Other State: {otherState}</span>
</div>
);
};
React 18 introduced automatic batching, which provides better performance by batching state updates.
Memoization and Performance Optimizations
Memoizing State Calculations
const MemoizedCounter = () => {
const [count, setCount] = useState(0);
const [otherState, setOtherState] = useState(0);
// Memoize the expensive calculation
const expensiveCalculation = useMemo(() => {
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += i;
}
return result;
}, []);
const handleIncrement = () => {
setCount(prev => prev + 1);
};
const handleOtherStateChange = () => {
setOtherState(prev => prev + 1);
};
return (
<div>
<button onClick={handleIncrement}>Increment Count</button>
<button onClick={handleOtherStateChange}>Change Other State</button>
<p>Count: {count}</p>
<p>Other State: {otherState}</p>
<p>Expensive Calculation: {expensiveCalculation}</p>
</div>
);
};
Use useMemo
sparingly. It can improve performance, but overuse can lead to
unnecessary complexity.
Memoizing Component Re-renders
interface CounterProps {
initialCount: number;
}
const MemoizedCounterComponent = memo(({ initialCount }: CounterProps) => {
const [count, setCount] = useState(initialCount);
const handleIncrement = () => {
setCount(prev => prev + 1);
};
return (
<div>
<button onClick={handleIncrement}>Increment</button>
<span>{count}</span>
</div>
);
});
Use React.memo
to memoize functional components and prevent unnecessary
re-renders.
Asynchronous State Updates
const AsyncCounter = () => {
const [count, setCount] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const handleIncrementAsync = async () => {
setIsLoading(true);
try {
// Simulate an asynchronous operation
await new Promise(resolve => setTimeout(resolve, 2000));
setCount(prev => prev + 1);
} finally {
setIsLoading(false);
}
};
return (
<div>
<button onClick={handleIncrementAsync} disabled={isLoading}>
{isLoading ? 'Loading...' : 'Increment Async'}
</button>
<span>{count}</span>
</div>
);
};
Always handle loading and error states when dealing with asynchronous operations.
Lazy Initialization
interface UserSettings {
theme: 'light' | 'dark';
fontSize: number;
locale: string;
}
const useUserSettings = () => {
const [userSettings, setUserSettings] = useState<UserSettings>(() => {
// Simulate fetching user settings from the server
return {
theme: 'light',
fontSize: 16,
locale: 'en-US'
};
});
const updateSettings = (newSettings: Partial<UserSettings>) => {
setUserSettings(prev => ({
...prev,
...newSettings
}));
};
return [userSettings, updateSettings] as const;
};
const SettingsPanel = () => {
const [userSettings, updateSettings] = useUserSettings();
return (
<div>
<label>
Theme:
<select
value={userSettings.theme}
onChange={e=>
updateSettings({ theme: e.target.value as 'light' | 'dark' })
}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</label>
{/* Other settings */}
</div>
);
};
Use the functional form of useState
to lazily initialize the state,
especially for expensive or asynchronous operations.
Conclusion
The useState
hook is the foundation of state management in React. By mastering advanced patterns like complex state updates, functional state updates, memoization, and lazy initialization, you can write more efficient, maintainable, and performant React applications.
Remember to:
- Carefully manage updates to nested objects and arrays
- Use the functional form of
setState
when your update depends on previous state - Leverage
useMemo
to memoize expensive calculations - Apply
React.memo
to memoize component re-renders - Handle loading and error states for asynchronous operations
- Use lazy initialization for expensive or asynchronous state initialization
As React continues to evolve, keep an eye out for new state management patterns and best practices.