You don’t know “useEffect”

Konstantine Kutalia
4 min readDec 28, 2023

--

Every React JS developer’s dream

There have been circulating many misconceptions about React JS hooks, particularly useEffect. Part of the problem is official React docs (now deprecated) which in many cases oversimplified matters to the point you asked yourself: “Why on earth React team doesn’t tell us how React actually works?!”. This caused massive confusion in the community, especially in junior/middle developers segment. When I conducted interviews for a company I could witness candidates over and over again not having a solid grasp on basic React component lifecycle concepts. Thanks gosh React team addressed the issue by releasing new docs. I started creating this post while the docs were still missing a lot, now I’ll try to make the topic even more accessible to save your time reading dozens of unfullfilling docs pages and contradictory StackOverflow answers. Let’s start:

useEffect is not same as (componentDidMount + componentWillUnmount + componentDidUpdate)

Yes, we’ve been lied. It’s a simple association, yet untrue on so many levels.

To understand differences between class component lifecycle methods and functional component hooks one must know the meaning and sequence of events in React JS and browsers like DOM mutation, paint, element render, re-render, mount, unmount, clean-up and etc.

Etimology 🤓📖

While there is an official MDN and React “glossary” for controversial phrases, many people still confuse terms like DOM paint with element render. I will try to interpret them simply, yet accurately:

  1. Render — if we want to differentiate render and paint then the term can be used to describe calculation process made by React JS in your memory which doesn’t result in any visualization just yet. It only prepares your element for painting by running code from your components, filtering what to show and whatnots using React’s reconcillation algorithm which is another fancy name for diffing browser’s DOM and virtual DOM.
  2. Mount — on this stage your components in a virtual DOM are actualized and inserted into a real DOM tree for the first time (which still takes time for browser engines to paint using native system calls like hardware acceleration, loading remote CSS assets, calculating styles and etc.). Future re-renders might cause certain children of a component to unmount, mount or update.
  3. Paint — the final stage of the browser rendering process resulting in the actual visualization of your elements. For some people mounting is analogous to painting for the first time, although as defined above and as we will see it later, from a pure technological viewpoint componentDidMount is fired before the actual first paint. By contrast, although it usually does, in specific scenarios useEffect code is not even guaranteed to only fire after paint (more on that later).
  4. Re-render — sometimes called update. Usually caused by state or props change, but can also be triggered by subscribing to the context API. Also can be bypassed by memoization. As the name implies, it causes the element to render again.
  5. Unmount — The antipode of mounting. the process of removing nodes rendered by your component from the DOM tree.

Here’s the proof that useEffect clean up runs after componentWillUnmount (see the console):

The simple explanation is that useEffect cleanup does not “live” in the same place as it’s React class sibling method.

Confusing, right?

Here’s a readable infographic showing what’s actually going on with the hook.

Simplified React component lifecycle and involving useEffect hook

As it looks like, both useEffect and useLayoutEffect cleanup functions always run before the next effect is executed (from the same hook).

It’s a different game when running actual effects:

useEffect is deferred, not blocking the “browser painting thread” (given that most browser engines handle the process with a single thread). Thus, if you update/read DOM inside useEffect, you might get caught in a process of a browser painting content from the latest React component render, resulting in a flicker/incorrect read respectively.

This is where useLayoutEffect comes in, which simply blocks the thread and executes before a browser has any chance to paint. If you mutate DOM there, browser engine will give priority to your values over the scheduled DOM mutations from the last render (segment 3 on the photo).

As of the useEffect not being guaranteed to only fire after paint, here’s the corner case:

import React, { useEffect, useRef } from 'react';

function ExampleComponent() {
const containerRef = useRef(null);

useEffect(() => {
const prePaintTask = () => {
// Execute some code before the browser paints the DOM
performPrePaintTask();

// Once the pre-paint task is complete, you can force an immediate re-render
// to ensure that the updated UI is painted without any delay
// Use requestAnimationFrame to schedule the re-render
window.requestAnimationFrame(() => {
// Update state or trigger other UI-related actions
});
};

// Schedule the pre-paint task using requestAnimationFrame
window.requestAnimationFrame(prePaintTask);
}, []);

return (
<div ref={containerRef}>
{/* Render your component's UI */}
</div>
);
}

export default ExampleComponent;

This happens because according to MND window.requestAnimationFrame() requests the browser to call a user-supplied callback function before the next repaint.

Hope this article gave you a clearer view of the React hooks, the component lifecycle and the browser painting mechanism.

While some of the techniques discussed here are more advanced, I recommend following general React guidelines and proven programming patterns. From my experience, oftentimes you don’t really benefit from the amount of control React gives with it’s API. And if your project desperately needs it, you might be doing something wrong.

--

--

Konstantine Kutalia

Aspiring software engineer specializing in front-end development and a guitar player.