5 React Hooks Techniques to Improve Component Performance
Practical Strategies for Building Faster and More Predictable React Applications
Modern React development has largely shifted toward functional components powered by Hooks. Since their introduction in React 16.8, Hooks have replaced many class-based patterns and enabled developers to write cleaner and more composable components.
However, using Hooks does not automatically guarantee good performance. In fact, poorly applied Hooks can easily introduce unnecessary renders, expensive recalculations, and subtle memory leaks. Many applications that appear simple at first gradually slow down as their component tree grows and state interactions become more complex.
Performance issues in React usually come from three common sources:
repeated expensive computations during rendering
unstable function references causing child components to re-render
effects that are not properly cleaned up
This article explores five practical techniques for improving React performance using Hooks. The goal is not premature optimization, but rather writing components that scale well as applications grow.
The techniques discussed include:
Using
useMemostrategically to avoid expensive recalculationsStabilizing function references with
useCallbackEncapsulating reusable logic in custom Hooks
Managing complex state with
useReducerPreventing memory leaks with proper
useEffectcleanup
Along the way, we will improve the original examples with clearer patterns, better naming, and real-world use cases.
1. Use useMemo Carefully to Avoid Expensive Recalculations
useMemo allows React to cache the result of a computation and reuse it during subsequent renders. This prevents the computation from running again unless its dependencies change.
However, a common mistake is using useMemo for trivial operations. The Hook itself has overhead, so applying it to simple calculations can actually reduce performance.
Incorrect Usage
The following example memoizes a simple multiplication:
const doubledValue = useMemo(() => counter * 2, [counter]);This is unnecessary. Multiplying a number is extremely cheap, and memoization adds complexity without any measurable benefit.
Appropriate Usage
useMemo becomes valuable when working with expensive operations such as:
filtering large datasets
sorting complex collections
performing CPU-heavy transformations
Example:
import { useMemo } from “react”;
function useFilteredUsers(users: User[]) {
return useMemo(() => {
return users
.filter(user => {
return (
user.isActive &&
user.rating > 80 &&
user.labels.includes(”priority”)
);
})
.sort((a, b) => b.rating - a.rating);
}, [users]);
}Now the filtering and sorting logic only runs when the users array changes.
Using It Inside a Component
function Dashboard({ users }) {
const visibleUsers = useFilteredUsers(users);
return (
<ul>
{visibleUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}When to Use useMemo
Consider memoization when:
the computation is expensive
the input dependencies change infrequently
the result is reused multiple times during rendering
Avoid using it simply because it exists. Profiling should guide optimization decisions.
2. Stabilize Function References with useCallback
One subtle performance issue in React occurs when functions are recreated on every render. Normally this is harmless, but problems arise when those functions are passed to child components.
Every new function reference causes React to treat the prop as changed, which may trigger unnecessary re-renders.
Problematic Example
function ProductPage({ productId }) {
const handleBuy = () => {
console.log(”Buying product:”, productId);
};
return <BuyButton onBuy={handleBuy} />;
}Even if productId does not change, handleBuy will be recreated on every render.
Optimized Version with useCallback
import { useCallback } from “react”;
function ProductPage({ productId }) {
const handleBuy = useCallback(() => {
console.log(”Buying product:”, productId);
}, [productId]);
return <BuyButton onBuy={handleBuy} />;
}Now the function reference remains stable unless productId changes.
Combining with React.memo
To maximize the effect, combine useCallback with React.memo.
const BuyButton = React.memo(function BuyButton({ onBuy }) {
console.log(”Button rendered”);
return (
<button onClick={onBuy}>
Buy Product
</button>
);
});If the parent component re-renders but onBuy stays the same, BuyButton will not re-render.
When useCallback Matters
Use useCallback when:
passing callbacks to memoized child components
callbacks are dependencies of other Hooks
stable references prevent unnecessary renders
Avoid using it everywhere. If a function is not passed as a prop or dependency, memoization is usually unnecessary.
3. Extract Reusable Logic into Custom Hooks
As React applications grow, many components begin to repeat similar logic:
local storage persistence
API fetching
event listeners
form handling
Instead of duplicating code across components, custom Hooks provide a clean way to encapsulate reusable logic.
Example: A Persistent State Hook
Here is an improved useLocalStorage implementation.
import { useState } from “react”;
function useLocalStorageState<T>(key: string, defaultValue: T) {
const [value, setValue] = useState<T>(() => {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : defaultValue;
} catch {
return defaultValue;
}
});
const updateValue = (newValue: T | ((prev: T) => T)) => {
try {
const valueToStore =
typeof newValue === “function”
? (newValue as (prev: T) => T)(value)
: newValue;
setValue(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(”Failed to store value:”, error);
}
};
return [value, updateValue] as const;
}Using the Custom Hook
function ThemeSwitcher() {
const [theme, setTheme] = useLocalStorageState(”theme”, “light”);
return (
<button onClick={() => setTheme(theme === “light” ? “dark” : “light”)}>
Current theme: {theme}
</button>
);
}Benefits of Custom Hooks
Custom Hooks provide several advantages:
reduce duplicated logic
simplify components
improve maintainability
encourage consistent behavior across the application
A well-designed Hook becomes a reusable building block.
4. Manage Complex State with useReducer
useState works well for simple state values. But as the number of state variables grows and updates become interconnected, managing everything with multiple useState calls can quickly become messy.
useReducer provides a more structured approach.
Example State
type CounterState = {
count: number;
loading: boolean;
error: string | null;
};Reducer Function
type CounterAction =
| { type: “increment” }
| { type: “decrement” }
| { type: “setLoading”; payload: boolean }
| { type: “setError”; payload: string | null };
function counterReducer(
state: CounterState,
action: CounterAction
): CounterState {
switch (action.type) {
case “increment”:
return { ...state, count: state.count + 1 };
case “decrement”:
return { ...state, count: state.count - 1 };
case “setLoading”:
return { ...state, loading: action.payload };
case “setError”:
return { ...state, error: action.payload };
default:
return state;
}
}Component Implementation
import { useReducer } from “react”;
const initialState: CounterState = {
count: 0,
loading: false,
error: null,
};
function Counter() {
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: “increment” })}>
Increase
</button>
<button onClick={() => dispatch({ type: “decrement” })}>
Decrease
</button>
</div>
);
}Why useReducer Helps Performance
Reducers centralize state transitions, making them:
easier to debug
easier to test
easier to reason about
For complex components or large forms, this pattern often produces cleaner code than multiple useState calls.
5. Prevent Memory Leaks with useEffect Cleanup
useEffect is used for side effects such as:
API requests
subscriptions
timers
event listeners
If these effects are not properly cleaned up, they can lead to memory leaks or unexpected behavior.
Example with Cleanup
import { useEffect, useState } from “react”;
function DataSubscriber({ api }) {
const [data, setData] = useState(null);
useEffect(() => {
const subscription = api.subscribe((payload) => {
setData(payload);
});
const timer = setInterval(() => {
console.log(”Heartbeat”);
}, 1000);
return () => {
subscription.unsubscribe();
clearInterval(timer);
};
}, [api]);
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}Why Cleanup Matters
Without cleanup:
timers continue running after unmount
event listeners accumulate
subscriptions leak memory
Returning a cleanup function ensures resources are released when the component unmounts or dependencies change.
Real-World Example: Optimizing a List Component
Consider a component that renders a large list of items.
A naive implementation may cause unnecessary re-renders.
Optimized Version
import { useMemo, useCallback } from “react”;
function OptimizedList({ items }) {
const visibleItems = useMemo(() => {
return items.filter(item => item.visible);
}, [items]);
const handleSelect = useCallback((itemId: string) => {
console.log(”Selected item:”, itemId);
}, []);
return (
<div>
{visibleItems.map(item => (
<ListItem
key={item.id}
item={item}
onSelect={handleSelect}
/>
))}
</div>
);
}Memoized Child Component
const ListItem = React.memo(function ListItem({ item, onSelect }) {
return (
<div onClick={() => onSelect(item.id)}>
{item.name}
</div>
);
});Result
This implementation improves performance by:
caching filtered results with
useMemostabilizing callbacks with
useCallbackpreventing child re-renders with
React.memo
These techniques become increasingly important when rendering hundreds or thousands of elements.
Conclusion
React Hooks provide powerful tools for building flexible and maintainable components, but using them effectively requires understanding how they affect rendering behavior.
The most important performance techniques include:
using
useMemoonly for expensive computationsstabilizing callbacks with
useCallbackextracting shared logic into custom Hooks
structuring complex state with
useReducercleaning up side effects inside
useEffect
The key principle is to optimize based on real performance bottlenecks rather than assumptions. React DevTools includes a Profiler that helps identify components that re-render frequently or take too long to render.
By measuring performance and applying these patterns strategically, developers can build React applications that remain fast and responsive even as they grow in complexity.


