Boost React Performance: Master Functional Components, Hooks, and Memoization
This article explains how functional components, React hooks, and memoization techniques such as React.memo, useCallback, and useMemo can be combined to reduce unnecessary re‑renders, simplify side‑effect management, and improve overall UI performance in modern React applications.
1. Introduction
Functional components provide a concise, data‑driven way to build UI. By splitting a React component into three parts—data, computation, and rendering—we can identify several performance‑optimization directions.
Data: use caching to reduce the number of rerender calls.
Computation: precisely determine when and how much to update, reducing computation load.
Render: fine‑grained rendering lowers component complexity.
Today we mainly share data‑layer performance‑optimization techniques.
1.1 What can Hooks do that class components cannot?
While learning the React
hook api, I discovered that hooks are more abstract and flexible than class lifecycle methods. The React FAQ asks a fascinating question— What can Hooks do that class components cannot?
Recently I found an answer while developing a feature that required passing a parameter via URL or cache to a newly opened tab. When the tab page loads, it reads the parameter and uses it to request additional data for rendering.
Initially I implemented the feature with a class component, but the code was hard to understand and maintain. Refactoring to a functional component made the code much cleaner.
1.2 A bad example (getDerivedStateFromProps + componentDidUpdate)
First I tried to use
getDerivedStateFromPropsand
componentDidUpdateto compare props and control requests and updates.
We have a parameter called
productId(also known as 商品id) that comes from the URL or cache. When the parent component receives an updated productId, it passes it to the child component via props.
<code>function Parent() {
/**
* When productId updates, pass it to Child
*/
return <Child productId={productId} />
}
</code>When the parent changes the
productIdprop, the child uses
getDerivedStateFromPropsto detect the change, updates its state, and triggers a re‑render.
<code>static getDerivedStateFromProps(nextProps, prevState) {
const { productId } = nextProps;
if (productId !== prevState.productId) {
return { productId };
}
return null;
}
</code>After the state update,
componentDidUpdatesends another request to fetch the product details.
<code>componentDidUpdate(prevProps, prevState) {
// When state changes, request new data
if (prevState.productId !== this.state.productId) {
this.getProductDetailRequest();
}
}
getProductDetailRequest = async () => {
const { productId } = this.state;
const { result } = await getProductDetail({ f_id: +productId });
this.setState({ productDetail: result });
};
</code>This implementation has several drawbacks:
Calling setState inside componentDidUpdate without proper guards can cause infinite loops.
State comparison is scattered across two lifecycle methods, making maintenance harder.
The demo code is overly complex for a simple use case.
1.3 Another bad example (componentWillReceiveProps)
Because
getDerivedStateFromPropsis static and cannot access
this, side‑effects such as async requests cannot be placed there. Therefore we might fall back to
componentWillReceivePropsto trigger a request when new props arrive.
<code>componentWillReceiveProps(props) {
const { productId } = props;
if (`${productId}` === 'null') return; // no request
if (productId !== this.state.productId) {
this.getProductDetailRequest(productId);
}
}
</code>This approach works only for React versions prior to 16.4.
1.4 Managing side‑effects in class components
Two common solutions exist, each with drawbacks:
componentWillReceiveProps was marked unsafe in React 16.4 because it is easy to misuse and introduce side‑effects.
Combining getDerivedStateFromProps with componentDidUpdate adds complexity without real benefit.
Since React 16.8, functional components with hooks provide a cleaner way to handle side‑effects.
2. Functional Component Performance Optimizations
2.1 Pure Components
Pure components stem from the concept of pure functions: the output depends only on the input and is deterministic. In React, a component that renders the same UI for identical props and state can be treated as pure.
2.1.1 Shallow Comparison
Shallow comparison checks primitive values for equality and reference values for identical memory addresses. It runs before the diff algorithm, preventing unnecessary re‑renders with less overhead.
2.1.2 Pure Component APIs
Pure components perform a shallow comparison of props and state, similar to implementing
shouldComponentUpdatemanually.
Class components can extend
PureComponent; functional components can be wrapped with
React.memo.
<code>import React, { PureComponent } from 'react';
class App extends PureComponent {}
export default App;
</code>If deep changes occur in referenced data, a pure component will not update, so custom
shouldComponentUpdatemay be needed.
2.1.3 React.memo
React.memois a higher‑order component that memoizes the result of a functional component based on shallow prop comparison.
<code>function MyComponent(props) {
/* render using props */
}
function areEqual(prevProps, nextProps) {
// custom comparison logic
}
export default React.memo(MyComponent, areEqual);
</code>When a component uses
useState,
useReducer, or
useContext, changes in state or context will still cause a re‑render even if props are unchanged.
2.1.4 Summary
Converting class components to functional components and applying
React.memois the most convenient way to create pure components.
2.2 useCallback
Passing callback functions via props can cause child components to re‑render on every parent render. Wrapping the callback with
useCallbackmemoizes it, updating only when its dependencies change.
<code>const Parent = () => {
const [title, setTitle] = useState('Title');
const callback = () => {
setTitle('Changed Title');
};
const memoizedCallback = useCallback(callback, []);
return (<>
<h1>{title}</h1>
<Child onClick={memoizedCallback} />
</>);
};
const Child = ({ onClick }) => (
<button onClick={onClick}>change title</button>
);
</code> useCallback(fn, deps)is equivalent to
useMemo(() => fn, deps).
2.3 useMemo
useMemocaches the result of an expensive calculation and recomputes only when its dependencies change.
<code>function computeExpensiveValue() {
// long calculation
return xxx;
}
const memoizedValue = useMemo(computeExpensiveValue, [a, b]);
</code>Example with factorial calculation:
<code>export function CalculateFactorial() {
const [number, setNumber] = useState(1);
const [inc, setInc] = useState(0);
const factorial = useMemo(() => factorialOf(number), [number]);
const onChange = event => setNumber(Number(event.target.value));
const onClick = () => setInc(i => i + 1);
return (
<div>
Factorial of <input type="number" value={number} onChange={onChange} /> is {factorial}
<button onClick={onClick}>Re‑render</button>
</div>
);
}
function factorialOf(n) {
console.log('factorialOf(n) called!');
return n <= 0 ? 1 : n * factorialOf(n - 1);
}
</code>When
incchanges, the component re‑renders, but because
numberis unchanged, the memoized factorial value is reused.
3. Conclusion
1. Combining functional components with the hook API offers a cleaner way to manage side‑effects, making it the preferred approach for the scenarios discussed in the introduction.
2. In plain terms,
React.memocaches the virtual DOM,
useCallbackcaches functions, and
useMemocaches values.
Tencent IMWeb Frontend Team
IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.