PureComponent vs Hooks: Mastering React Re‑renders and Performance
This article explores how PureComponent and shouldComponentUpdate address unnecessary re‑renders in class components, compares them with functional components and hooks, and provides practical techniques—including React.memo, useCallback, setState updater functions, and refs—to optimize rendering performance in modern React applications.
The article examines the evolution of React performance optimization, starting with class‑based PureComponent and
shouldComponentUpdate, and then showing how equivalent behavior can be achieved with functional components, hooks, and memoization.
PureComponent, shouldComponentUpdate: what problems they solve
Unnecessary re‑renders often originate from a parent component updating its state, which forces child components to render again.
<code>const Child = () => <div>render something here</div>;
const Parent = () => {
const [counter, setCounter] = useState(1);
return (
<>
<button onClick={() => setCounter(counter + 1)}>Click me</button>
{/* child will re‑render when "counter" changes */}
<Child />
</>
);
};</code>The same effect occurs with class components:
<code>class Child extends React.Component {
render() {
return <div>render something here</div>;
}
}
class Parent extends React.Component {
constructor() {
super();
this.state = { counter: 1 };
}
render() {
return (
<>
<button onClick={() => this.setState({ counter: this.state.counter + 1 })}>Click me</button>
{/* child will re‑render when state changes */}
<Child />
</>
);
}
};</code>To stop these re‑renders, class components provide
shouldComponentUpdate. Returning
falseprevents the component from updating:
<code>class Child extends React.Component {
shouldComponentUpdate() {
// now child component won't ever re‑render
return false;
}
render() {
return <div>render something here</div>;
}
};</code>If a component needs to react to specific prop changes,
shouldComponentUpdatecan compare
nextProps(and optionally
nextState) with the current values:
<code>class Child extends React.Component {
shouldComponentUpdate(nextProps) {
if (nextProps.someprop !== this.props.someprop) return true;
return false;
}
render() {
return <div>{this.props.someprop}</div>;
}
};</code> <code>shouldComponentUpdate(nextProps, nextState) {
if (nextProps.someprop !== this.props.someprop) return true;
if (nextState.somestate !== this.state.somestate) return true;
return false;
}</code>Manually writing deep comparisons is error‑prone, so React offers
React.PureComponentwhich implements a shallow prop and state check automatically:
<code>class PureChild extends React.PureComponent {
constructor(props) {
super(props);
this.state = { somestate: 'nothing' };
}
render() {
return (
<div>
<button onClick={() => this.setState({ somestate: 'updated' })}>Click me</button>
{this.state.somestate}
{this.props.someprop}
</div>
);
}
};</code>Using
PureChildinside a parent prevents re‑renders caused by the parent’s state changes:
<code>class Parent extends React.Component {
constructor() {
super();
this.state = { counter: 1 };
}
render() {
return (
<>
<button onClick={() => this.setState({ counter: this.state.counter + 1 })}>Click me</button>
{/* child will NOT re‑render when state changes */}
<PureChild someprop="something" />
</>
);
}
};</code>PureComponent/shouldComponentUpdate vs functional components & hooks
Function components cannot use
shouldComponentUpdateor extend
PureComponent, but
React.memoprovides the same shallow‑compare behavior.
<code>const Child = ({ someprop }) => {
const [something, setSomething] = useState('nothing');
return (
<div>
<button onClick={() => setSomething('updated')}>Click me</button>
{something}
{someprop}
</div>
);
};
export const PureChild = React.memo(Child);
</code>When the parent’s state changes,
PureChilddoes not re‑render, mirroring the class‑based
PureComponentbehavior:
<code>const Parent = () => {
const [counter, setCounter] = useState(1);
return (
<>
<button onClick={() => setCounter(counter + 1)}>Click me</button>
{/* won’t re‑render because of counter change */}
<PureChild someprop="123" />
</>
);
};</code>When a prop is a function (e.g., an
onClickhandler), the function reference changes on every parent render, breaking memoization. A custom comparison function can exclude such props:
<code>const areEqual = (prevProps, nextProps) => prevProps.someprop === nextProps.someprop;
export const PureChild = React.memo(Child, areEqual);
</code>In practice, developers often prefer
useCallbackto memoize callbacks, optionally adding state dependencies:
<code>const Parent = () => {
const onChildClick = () => { /* do something */ };
const onChildClickMemo = useCallback(onChildClick, []);
return <PureChild someprop="something" onClick={onChildClickMemo} />;
};
</code>If the callback needs to read the latest state, include that state in the dependency array:
<code>const Parent = () => {
const [counter, setCounter] = useState(1);
const onChildClick = () => {
if (counter > 100) return;
// do something
};
const onChildClickMemo = useCallback(onChildClick, [counter]);
return <PureChild someprop="something" onClick={onChildClickMemo} />;
};
</code>Alternatively, use the functional form of
setStateto avoid listing state in the dependency array:
<code>const onChildClick = () => {
setCounter(counter => {
if (counter > 100) return counter;
return counter + 1;
});
};
</code>Another pattern stores mutable data in a
ref, which does not trigger re‑renders when updated:
<code>const Parent = () => {
const [counter, setCounter] = useState(1);
const mirrorStateRef = useRef(null);
useEffect(() => { mirrorStateRef.current = counter; }, [counter]);
const onChildClick = () => {
if (mirrorStateRef.current > 100) return;
// do something
};
const onChildClickMemo = useCallback(onChildClick, []);
return <PureChild someprop="something" onClick={onChildClickMemo} />;
};
</code>For props that are arrays or objects, memoization (via
useMemoor external libraries) is required to keep the reference stable across renders:
<code>const someArray = useMemo(() => ([1, 2, 3]), []);
<PureChild someArray={someArray} />;
</code>Skipping state updates – a quirky behavior
React will still invoke a component’s render function once after a state update, even if the new state equals the previous one. This ensures that effects run safely, but subsequent identical updates are ignored, preventing unnecessary re‑renders.
<code>const Parent = () => {
const [state, setState] = useState(0);
console.log('Log parent re‑renders');
return (
<>
<button onClick={() => setState(1)}>Click me</button>
</>
);
};
</code>The first click changes state from 0 to 1 and logs; the second click attempts to set 1 → 1, logs again because React still renders once, but further identical updates produce no log.
Conclusion
PureComponent can be replaced by wrapping functional components with
React.memoto achieve identical re‑render control.
shouldComponentUpdateprop‑comparison logic is expressed in
React.memovia an optional custom comparator.
Function components no longer need to worry about unnecessary state updates; React handles them efficiently.
When passing callbacks to memoized components, use
useCallback, the functional updater form of
setState, or
refto keep the reference stable.
Array and object props should be memoized (e.g., with
useMemo) to avoid breaking memoization.
KooFE Frontend Team
Follow the latest frontend updates
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.