React js 18 Synchronizing with Effects - Complete guide

Synchronizing with Effects

Some components need to synchronize with external systems. For example, you might want to control a non-React component based on the React state, set up a server connection, or send an analytics log when a component appears on the screen. Effects let you run some code after rendering so that you can synchronize your component with some system outside of React.

You will learn

  • What effects are
  • How effects are different from events
  • How to declare an effect in your component
  • Why effects run twice in development mode
  • How to optimize effects with dependencies

What are effects and how are they different from events?

Before getting to effects, you need to be familiar with two types of logic inside React components:

  • Rendering code (introduced in Describing the UI) lives at the top level of your component. This is where you take the props and state, transform them, and return the JSX you want to see on the screen. Rendering code must be pure. Like a math formula, it should only calculate the result, but not do anything else.

  • Event handlers (introduced in Adding Interactivity) are nested functions inside your components that do things rather than just calculate them. An event handler might update an input field, submit an HTTP POST request to buy a product, or navigate the user to another screen. Event handlers contain “side effects” (they change the program’s state) and are caused by a specific user action (for example, a button click or typing).

Sometimes this isn’t enough. Consider a ChatRoom component that must connect to the chat server whenever it’s visible on the screen. Connecting to a server is not a pure calculation (it’s a side effect) so it can’t happen during rendering. However, there is no single particular event like a click that causes ChatRoom to be displayed.

Effects let you specify side effects that are caused by rendering itself, rather than by a particular event. Sending a message in the chat is an event because it is directly caused by the user clicking a specific button. However, setting up a server connection is an effect because it needs to happen regardless of which interaction caused the component to appear. Effects run at the end of the rendering process after the screen updates. This is a good time to synchronize the React components with some external system (like network or a third-party library).

You might not need an effect

Don’t rush to add effects to your components. Keep in mind that effects are typically used to “step out” of your React code and synchronize with some external system. This includes browser APIs, third-party widgets, network, and so on. If your effect only adjusts some state based on other state, you might not need an effect.

How to write an effect

To write an effect, follow these three steps:

  1. Declare an effect. By default, your effect will run after every render.
  2. Specify the effect dependencies. Most effects should only re-run when needed rather than after every render. For example, a fade-in animation should only trigger when a component appears. Connecting and disconnecting to a chat room should only happen when the component appears and disappears, or when the chat room changes. You will learn how to control this by specifying dependencies.
  3. Add cleanup if needed. Some effects need to specify how to stop, undo, or clean up whatever they were doing. For example, “connect” needs “disconnect,” “subscribe” needs “unsubscribe,” and “fetch” needs either “cancel” or “ignore”. You will learn how to do this by returning a cleanup function.

Let’s look at each of these steps in detail.

Step 1: Declare an effect

To declare an effect in your component, import the useEffect Hook from React:

 
import { useEffect } from 'react';

Then, call it at the top level of your component and put some code inside your effect:

 
function MyComponent() {
useEffect(() => {
// Code here will run after *every* render
});
return <div />;
}

Every time your component renders, React will update the screen and then run the code inside useEffect. In other words, useEffect “delays” a piece of code from running until that render is reflected on the screen.

Let’s see how you can use an effect to synchronize with an external system. Consider a <VideoPlayer> React component. It would be nice to control whether it’s playing or paused by passing an isPlaying prop to it:

 
<VideoPlayer isPlaying={isPlaying} />;

Your custom VideoPlayer component renders the built-in browser <video> tag:

 
function VideoPlayer({ src, isPlaying }) {
// TODO: do something with isPlaying
return <video src={src} />;
}

However, the browser <video> tag does not have an isPlaying prop. The only way to control it is to manually call the play() and pause() methods on the DOM element. You need to synchronize the value of isPlaying prop, which tells whether the video should currently be playing, with imperative calls like play() and pause().

We’ll need to first get a ref to the <video> DOM node.

The reason this code isn’t correct is that it tries to do something with the DOM node during rendering. In React, rendering should be a pure calculation of JSX and should not contain side effects like modifying the DOM.

Moreover, by the time VideoPlayer is called for the first time, its DOM does not exist yet! There isn’t any DOM node yet to call play() or pause() on, because React doesn’t know what DOM to create until after you return the JSX.

The solution here is to wrap the side effect with useEffect to move it out of the rendering calculation:

By wrapping the DOM update in an effect, you let React update the screen first. Then your effect runs.

When your VideoPlayer component renders (either the first time or if it re-renders), a few things will happen. First, React will update the screen, ensuring the <video> tag is in the DOM with the right props. Then React will run your effect. Finally, your effect will call play() or pause() depending on the value of isPlaying prop.

 

Press Play/Pause multiple times and see how the video player stays synchronized to the isPlaying value:

In this example, the “external system” you synchronized to React state was the browser media API. You can use a similar approach to wrap legacy non-React code (like jQuery plugins) into declarative React components.

(Note that controlling a video player is much more complex in practice. Calling play() may fail, the user might play or pause using the built-in browser controls, and so on. This example is very simplified and incomplete.)