How did I re-render: Eliminate unnecessary React Effects
The useEffect
hook bears the risk of re-execution of React Lifecycle, if improperly utilized. A few use cases can be identified to mitigate such scenarios.
Phases of React Lifecycle
React’s functionality is split into two phases, namely: Render, & Commit phase lifecycles, which run synchronously and update the UI.
- During the ‘render’ phase, React traverses its Component Tree and maintains a Virtual DOM via a process called ‘reconciliation’.
- At the end of the ‘render’ phase, React compares its Virtual DOM to the DOM, and identifies the list of changes it needs to perform on the Browser DOM.
- Afterward, React ‘commits’ the identified list of changes to the Browser DOM.
Escape hatch from the React paradigm
Through the implementation of Effects through the useEffect
hook provided by React, updations & side-effects can be managed after the ‘commit’ phase.
This is extremely helpful to synchronize an external non-react system with React updates, e.g. consider synchronizing an HTML5 element (a video) with a React State. It is not directly possible as a video tag requires the execution of play()
or pause()
for interactions.
This can be achieved by maintaining a ref through useRef
hook, and performing side-effects in useEffect
hook upon state updation.
function App () { const ref = useRef(null); const [isPlaying, setIsPlaying] = useState(false);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
});return (
<>
<button onClick={() => setIsPlaying(!isPlaying)}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<video
ref={ref}
src='https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4'
loop
playsInline
/>
</>
)
}
Improper Usage of Effects
useEffect
hook allows a dependency array to avoid re-execution, but if a React State is updated in an Effect, the React Lifecycle is re-iterated and both ‘render’ and ‘commit’ phases are performed again.
For example, in a scenario to update a state based on props, one might be tempted to do the following:
function App({firstName, lastName}) {
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}
The aforementioned approach solves the problem but causes React to re-render just after it has completed its ‘commit’ phase. Instead, a better approach would be to derive a variable inside the component.
function App({firstName, lastName}) {
const [fullName, setFullName] = useState('');
const fullName = firstName + ' ' + lastName;
// ...
}
Therefore, to avoid repeated React ‘commit’ phases, the React Effects are only to be used to synchronize with non-React APIs or Widgets.
Below are a few examples of such cases where the useEffect
hook can be completely eliminated in favor of performance.
Initializing the application
Often it is required to execute a code segment only once to initialize the application. A common paradigm dictates the usage of useEffect
hook with an empty dependency array should solve such a scenario.
function Root() {
useEffect(() => {
initApp();
}, []);
// ...
}
But, the aforementioned paradigm would cause the code segment to execute twice in development.
Since the code segment is to be executed only once when the application is to be initialized, it can safely be executed during module initialization and before the React renders.
if (typeof window !== 'undefined') {
initApp();
}function Root() {
// ...
}
Resetting React Component
There may arise a situation where a Component would need to ‘reset’ itself if a prop changes. One might be tempted to do so by resetting all the internal states of a Component in an Effect as follows:
function App({ uid }) {
const [data, setData] = useState('');useEffect(() => {
setData('');
}, [uid]);
// ...
}function Root({uid}) {
return <App uid={uid} />
}
Though this would work, it will cause React to begin the lifecycle phases again, causing a re-render immediately after a ‘commit’ phase has been issued. Furthermore, for more complex components keeping track of props and internal states would be a manual error-prone effort.
Instead, through the use key
prop, React provides the ability to uniquely identify a Component. Whenever the key
prop changes, React realizes that the Component identity is changed and therefore forces a re-instantiation of the Component, effectively resetting the component.
function App({ uid }) {
const [data, setData] = useState('');
// ...
}function Root({uid}) {
return <App key={uid} uid={uid} />
}
NOTE: React uses the item’s index in the array as its key if the key is not specified, and React Components won’t receive a key as a prop.
Summary
useEffect
should not be used to perform synchronous actions, like data manipulations, to avoid unnecessary ‘render’ passes. The transformations at the top level of your components will be re-run upon updation.- Updation of React State should be avoided in the
useEffect
hook, to avoid re-rendering after the ‘commit’ phase. The only exception is when the React State is to be synchronized with a non-React system like Web APIs, or third-party widgets.
For further reading: https://react.dev/learn/you-might-not-need-an-effect
Other articles in the series: