Why Do Function Components Re‑Render? A Deep Dive into Hooks and Optimization
This article explains the three main reasons function components re‑render—useState/useReducer updates, parent updates, and context changes—and shows how to control unnecessary renders with techniques such as memoization, useCallback, useRef, and component extraction.
Function component re‑render can be triggered by three main situations: updates via
useState/
useReducer, parent component updates, and context changes.
1. Updates caused by useState or useReducer
1.1 Regular usage
Example Counter component that logs "counter render" on each click.
<code>const Counter = () => {
console.log('counter render');
const [count, addCount] = useState(0);
return (
<div className="counter">
<div className="counter-num">{count}</div>
<button onClick={() => {addCount(count + 1)}}>add</button>
</div>
);
}</code>1.2 Immutable state
When state is a reference type, React compares old and new state with
Object.is; if they are equal, no re‑render occurs.
<code>const Counter = () => {
console.log('counter render');
const [count, addCount] = useState({ num: 0, time: Date.now() });
const clickHandler = () => {
count.num++;
count.time = Date.now();
addCount(count);
};
return (
<div className="counter">
<div className="counter-num">{count.num}, {count.time}</div>
<button onClick={clickHandler}>add</button>
</div>
);
}</code>Therefore state must be immutable and a new value must be returned for an update to be effective.
1.3 Forced update
Function components lack
forceUpdate, but you can simulate it by updating a dummy state created with
useState({}).
<code>const [, forceUpdate] = useState({});
forceUpdate({});</code>2. Parent component updates
2.1 Regular usage
Adding a child component
Hellocauses it to re‑render on every parent state change, even though its props do not change.
<code>const Hello = ({ name }) => {
console.log('hello render');
return <div>hello {name}</div>;
};
const App = () => {
console.log('app render');
const [count, addCount] = useState(0);
return (
<div className="app">
<Hello name="react" />
<div className="counter-num">{count}</div>
<button onClick={() => {addCount(count + 1)}}>add</button>
</div>
);
};</code>2.2 Optimizing component design
2.2.1 Extract updating part into a separate component
Move the counter logic into its own
Countercomponent so
Hellois not affected.
<code>const App = () => {
console.log('app render');
return (
<div className="app">
<Hello name="react" />
<Counter />
</div>
);
};</code>2.2.2 Use children slots for non‑changing parts
<code>const App = ({ children }) => {
console.log('app render');
const [count, addCount] = useState(0);
return (
<div className="app">
{children}
<div className="counter-num">{count}</div>
<button onClick={() => {addCount(count + 1)}}>add</button>
</div>
);
};
// Usage
<App>
<Hello name="react" />
</App></code>2.3 React.memo
Class components have
PureComponentand
shouldComponentUpdate. Function components can use
React.memoto skip re‑render when props are shallowly equal.
<code>const Hello = React.memo(({ name }) => {
console.log('hello render');
return <div>hello {name}</div>;
});
const App = () => {
console.log('app render');
const [count, addCount] = useState(0);
return (
<div className="app">
<Hello name="react" />
<div className="counter-num">{count}</div>
<button onClick={() => {addCount(count + 1)}}>add</button>
</div>
);
};</code>Memo uses
shallowEqualby default; if a prop is a newly created function each render, memo will still re‑render.
2.3.1 useCallback
Wrap callbacks with
useCallbackto keep the same reference, preventing unnecessary re‑renders.
<code>const clickHandler = useCallback(() => {
console.log('hello click');
}, []);</code>If the callback uses a state value, the dependency array must include that state; otherwise the callback captures a stale value.
2.3.2 useRef & useEffect
Store mutable values in a ref and update them in
useEffect; callbacks can read
ref.currentto get the latest state without changing their identity.
<code>const App = ({ children }) => {
console.log('counter render');
const [count, addCount] = useState(0);
const countRef = useRef(count);
const clickHandler = useCallback(() => {
console.log('count: ', countRef.current);
}, [countRef]);
useEffect(() => {
countRef.current = count;
}, [count]);
return (
<div className="counter">
<Hello name="react" onClick={clickHandler} />
<div className="counter-num">{count}</div>
<button onClick={() => {addCount(count + 1)}}>add</button>
</div>
);
};</code>Summary of the approach: use
useRefto hold changing values,
useEffectto sync them, and
useCallbackto return a stable function.
3. Context updates
Context changes also trigger re‑render; libraries like react‑redux and react‑router rely on this mechanism. For a deeper look, see the referenced article on React Context source analysis.
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.