Introduction
React Hooks revolutionized functional components by enabling state management and side effects without using classes. Beyond the basic hooks like useState and useEffect, React provides several advanced hooks that allow developers to handle complex state, optimize performance, and manage DOM references.
In this post, we will explore four advanced hooks in depth:
useReducerfor managing complex stateuseReffor accessing DOM elements and persisting valuesuseMemofor memoizing expensive calculationsuseCallbackfor memoizing functions to prevent unnecessary re-renders
Understanding these hooks is essential for building high-performance, maintainable, and scalable React applications.
useReducer: Managing Complex State
useReducer is a hook that is similar to useState but is particularly useful for managing complex state logic where multiple sub-values or actions need to be handled.
Basic Syntax
const [state, dispatch] = useReducer(reducer, initialState);
reduceris a function that takes the current state and an action, returning the next stateinitialStateis the starting state valuedispatchis a function used to send actions to the reducer
Example: Counter with Multiple Actions
import React, { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}
Advantages:
- Centralizes state logic in the reducer function
- Makes state transitions predictable
- Ideal for components with multiple actions
Example: Form State Management
For forms with multiple input fields:
const initialState = {
name: '',
email: '',
password: ''
};
function formReducer(state, action) {
return { ...state, [action.field]: action.value };
}
function SignupForm() {
const [state, dispatch] = useReducer(formReducer, initialState);
const handleChange = (e) => {
dispatch({ field: e.target.name, value: e.target.value });
};
return (
<form>
<input name="name" value={state.name} onChange={handleChange} />
<input name="email" value={state.email} onChange={handleChange} />
<input name="password" value={state.password} onChange={handleChange} />
<p>{JSON.stringify(state)}</p>
</form>
);
}
Benefits:
- Handles complex forms efficiently
- Avoids multiple
useStatecalls - Reduces code repetition
useRef: Accessing DOM Elements and Persisting Values
The useRef hook allows you to create a mutable object that persists across renders. It is commonly used to:
- Access DOM elements directly
- Store values without triggering re-renders
Basic Syntax
const refContainer = useRef(initialValue);
refContainer.currentholds the value- Updating
.currentdoes not trigger re-render
Example: Accessing a DOM Element
function TextInputFocus() {
const inputRef = React.useRef(null);
const focusInput = () => {
inputRef.current.focus(); // Focus the input element
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus Input</button>
</div>
);
}
Example: Persisting Values Across Renders
function Timer() {
const [count, setCount] = React.useState(0);
const countRef = React.useRef(count);
React.useEffect(() => {
countRef.current = count; // Store latest value
}, [count]);
const handleAlert = () => {
setTimeout(() => {
alert(Current count: ${countRef.current});
}, 3000);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={handleAlert}>Show Count After 3s</button>
</div>
);
}
Benefits of useRef:
- Persistent values without re-rendering
- Useful for timers, DOM access, and storing previous state
useMemo: Memoizing Expensive Calculations
useMemo is used to memoize the result of a computation, so that it only recalculates when dependencies change. This is particularly useful for expensive calculations.
Syntax
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
Example: Memoizing a Heavy Calculation
function ExpensiveComponent({ num }) {
const computeFactorial = (n) => {
console.log('Calculating factorial...');
return n <= 1 ? 1 : n * computeFactorial(n - 1);
};
const factorial = React.useMemo(() => computeFactorial(num), [num]);
return <p>Factorial of {num}: {factorial}</p>;
}
Key Points:
computeFactorialruns only whennumchanges- Prevents recalculating on every render
- Improves performance in complex UIs
Example: Filtering a Large List
function FilteredList({ items, search }) {
const filteredItems = React.useMemo(() => {
return items.filter(item => item.toLowerCase().includes(search.toLowerCase()));
}, [items, search]);
return (
<ul>
{filteredItems.map(item => (
<li key={item}>{item}</li>
))}
</ul>
);
}
Benefits:
- Avoids recomputation on unrelated renders
- Optimizes lists, tables, or heavy calculations
useCallback: Memoizing Functions
useCallback returns a memoized version of a function, preventing unnecessary re-creations on every render. It is useful when passing callbacks to child components to avoid unnecessary re-renders.
Syntax
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
Example: Preventing Child Re-Renders
function Child({ onClick }) {
console.log('Child rendered');
return <button onClick={onClick}>Click Me</button>;
}
function Parent() {
const [count, setCount] = React.useState(0);
const handleClick = React.useCallback(() => {
console.log('Button clicked');
}, []); // Function memoized
return (
<div>
<p>Count: {count}</p>
<Child onClick={handleClick} />
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Explanation:
- Without
useCallback,handleClickis recreated every render - Causes
Childcomponent to re-render unnecessarily - Memoizing avoids redundant renders, improving performance
Example: Dependency-Aware useCallback
function Parent({ multiplier }) {
const [count, setCount] = React.useState(0);
const multiply = React.useCallback(() => {
setCount(prev => prev * multiplier);
}, [multiplier]); // Only updates if multiplier changes
return (
<div>
<p>Count: {count}</p>
<button onClick={multiply}>Multiply</button>
</div>
);
}
Best Practices:
- Only memoize functions when necessary
- Avoid overusing
useCallbackas it can add complexity - Useful when passing callbacks to memoized children or expensive operations
Combining Advanced Hooks
These hooks often work together in complex components. Example:
function Dashboard({ users }) {
const [filter, setFilter] = React.useState('');
const [selectedUser, setSelectedUser] = React.useState(null);
const inputRef = React.useRef();
const filteredUsers = React.useMemo(() => {
return users.filter(user => user.name.toLowerCase().includes(filter.toLowerCase()));
}, [users, filter]);
const handleSelect = React.useCallback((user) => {
setSelectedUser(user);
}, []);
React.useEffect(() => {
inputRef.current.focus(); // Focus input on mount
}, []);
return (
<div>
<input ref={inputRef} value={filter} onChange={e => setFilter(e.target.value)} />
<ul>
{filteredUsers.map(user => (
<li key={user.id} onClick={() => handleSelect(user)}>
{user.name}
</li>
))}
</ul>
{selectedUser && <p>Selected: {selectedUser.name}</p>}
</div>
);
}
Highlights:
useStatemanages filter and selectionuseRefhandles input focususeMemooptimizes filteringuseCallbackmemoizes selection function
Leave a Reply