DEV Community

Ayako yk
Ayako yk

Posted on

The Lifecycle of useEffect: Synchronization in React

In previous blog posts, I discussed the foundation of the useEffect hook and when not to use it. In this post, I'll explore its lifecycle.

useEffect is primarily used to synchronize React components after side effects, which are operations outside of React's main rendering system. The Effect runs after the component mounts, so we should consider React's component lifecycle and useEffect's lifecycle separately. The React documentation mentions this in bold, which I'll share after this brief explanation.

React components go through three stages: mount, update, and unmount. However, useEffect needs to synchronize less frequently or more frequently than the component's cycle.

Here's an example from the React documentation. This component has a dropdown menu, where a user can choose a room ID: general, travel, or music.

function ChatRoom({ roomId /* "general" */ }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId); // Connects to the "general" room
    connection.connect();
    return () => {
      connection.disconnect(); // Disconnects from the "general" room
  };
}, [roomId]);
Enter fullscreen mode Exit fullscreen mode

Every time the roomId changes, the Effect stops synchronizing with the old roomId and starts synchronizing with the new one. React calls the cleanup function.
Therefore, we should consider React's components and useEffect independently.

The React documentation says:

[A]lways focus on a single start/stop cycle at a time. It shouldn’t matter whether a component is mounting, updating, or unmounting. All you need to do is to describe how to start synchronization and how to stop it. If you do it well, your Effect will be resilient to being started and stopped as many times as it’s needed.

How to Tell React to Re-Synchronize
We can tell React when to re-synchronize by providing props or state in the dependency array. An empty dependency array means useEffect runs only when the component mounts.

Each Effect Should Represent a Separate Synchronization Process
We might be tempted to simplify code by combining functions to make it "cleaner." However, we should keep in mind that we will likely add more features and functions later on. So, each useEffect should contain only one logical operation.

The ChatRoom component has two distinct operations: one is logging the user's visit to the page, and the other is connecting to the server. Currently, it might be fine to combine them, as both are triggered when the roomId changes. However, as the component gains more features and the dependency array grows, this could lead to undesired behavior.

function ChatRoom({ roomId }) {
  useEffect(() => {
    logVisit(roomId);

    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [roomId]);
  ...
}
Enter fullscreen mode Exit fullscreen mode

Instead, we should separate them:

function ChatRoom({ roomId }) {
  useEffect(() => {
    logVisit(roomId);
  }, [roomId]);

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    ...
}, [roomId]);
  ...
}
Enter fullscreen mode Exit fullscreen mode

Effect Should Re-Synchronize Whenever Reactive Values Change
Values declared in a component are all "reactive." This includes not only props and state but also variables that are calculated based on the change of props and state.

Reactive values should re-synchronize with the effect because they change over time. Therefore, we should include those values in the dependency array.

Here's an example from the React documentation:

function ChatRoom({ roomId, selectedServerUrl }) { // roomId is reactive
    const settings = useContext(SettingsContext); // settings is reactive
    const serverUrl = selectedServerUrl ?? settings.defaultServerUrl; //    serverUrl is reactive
    useEffect(() => {
      const connection = createConnection(serverUrl, roomId); // Your Effect reads roomId and serverUrl
      connection.connect();
      return () => {
        connection.disconnect();
      };
    }, [roomId, serverUrl]); // So it needs to re-synchronize when either of them changes!
  ...
}
Enter fullscreen mode Exit fullscreen mode

Linter Will Warn You
When a linter is configured for React, it will throw an error if a dependency array is missing values. The linter isn't perfect and won't automatically fix the issue, but we should carefully read the error message and fix the code.

To recap, we should consider the lifecycle of useEffect separately from the lifecycle of React components. Think of useEffect as handling two tasks: starting synchronization and stopping synchronization. Additionally, we should leverage the linter to catch potential issues, ensuring that we don't miss any bugs when building our application.

Top comments (0)