A deep dive into React effects

Learn about effects – a declarative approach to interacting with the world outside your app. With live demos, interactive exercises, and fractals.

Imagine that for some reason, you’ve decided to create a React component that renders an animated fractal tree. In fact, let’s say that you’re already 90% of the way there — you have a fractal tree component, and now you want to animate it.

App.js
FractalTreeFrame.js
FractalHelpers.js
index.js
import React, { useState } from 'react'
import FractalTreeFrame from './FractalTreeFrame'

// The height and width of the entire window
const { innerHeight, innerWidth } = window

export default function App() {
  const [mousePosition, setMousePosition] = useState({
    x: innerWidth / 2,
    y: innerHeight / 2,
  })

  return (
    <FractalTreeFrame
      mousePosition={mousePosition}
      onMouseMove={({ clientX: x, clientY: y }) =>
        setMousePosition({ x, y })
      }
      time={Date.now()}
    />
  )
}
Build In Progress

If you move the mouse around the preview area, you’ll see that the tree is already somewhat animated, due to the onMouseMove handler:

onMouseMove={({ clientX: x, clientY: y }) =>
  setMousePosition({ x, y })
}

Each time the mouse moves over the preview area, React calls this event handler. This in turn updates the component state, triggering a re-render of the component with a new value of the time prop.

time={Date.now()}

Of course, it won’t do to require the user to constantly move their mouse to keep the animation going. Ideally, the component would automatically schedule a new frame after each frame is rendered, using something like the browser’s requestAnimationFrame() function.

As it happens, there’s a pretty obvious way to make this work. Given that updating the state also triggers a re-render, you can store the current time with a useState hook. Then, you can trigger re-render by updating the time in a requestAnimationFrame() callback. To try it out, just uncomment lines 14–17 below:

App.js
FractalTreeFrame.js
FractalHelpers.js
index.js
import React, { useState } from 'react'
import FractalTreeFrame from './FractalTreeFrame'

// The height and width of the entire window
const { innerHeight, innerWidth } = window

export default function App() {
  const [time, setTime] = useState(Date.now())
  const [mousePosition, setMousePosition] = useState({
    x: innerWidth / 2,
    y: innerHeight / 2,
  })

  // window.requestAnimationFrame(() => {
  //   // Update time to trigger a re-render
  //   setTime(Date.now())
  // })

  return (
    <FractalTreeFrame
      mousePosition={mousePosition}
      onMouseMove={({ clientX: x, clientY: y }) =>
        setMousePosition({ x, y })
      }
      time={time}
    />
  )
}
Build In Progress

Once you’ve removed the comment above, this animation looks to work pretty well! That is, at least until you try moving the mouse over the tree as well. Go ahead, try quickly moving your mouse over the tree, and see how performance takes a nosedive.

What the hell is going on?

Here’s the problem: if the mouse moves, then React will update the state and re-render the component, thus scheduling another render in addition to the one already scheduled by requestAnimationFrame(). Now you have two scheduled renders, twice the CPU usage, half the frame rate, and everything starts to grind to a halt.

#So… effects?

In the above example, the call to window.requestAnimationFrame() is what’s called a side effect; it’s something that your component does in addition to returning elements.

Side effects are what make your application actually useful. They’re responsible for things like:

  • Loading data from the server
  • Setting and clearing timers
  • Interacting with the DOM

As you can see from the above example, it’s entirely possible to put effectful code directly in the component function. The thing is, the resulting effects occur each and every time the component function is called — which is rarely the desired behavior. More often, you’ll want the effect to occur in response to specific conditions — and to stop occurring once those conditions are removed.

#The useEffect() function

This hook function lets you ask React to do something after the component has finished rendering to the DOM. There are a few ways to use it, so let’s start with the simplest:

useEffect(() => {
  // do stuff after update
})

When useEffect() receives a function as its first and only argument, it’ll call that function on every update. You might use this to, for example, validate usernames in a social network’s onboarding sequence. You can see how I’ve approached this below — go ahead, try typing in a username to test it out!

App.js
getUsernameAva … .js
styles.css
index.js
import React, { useEffect, useState } from 'react'
import getUsernameAvailability from './getUsernameAvailability'

export default function App() {
  const [username, setUsername] = useState('')
  const [availability, setAvailability] = useState({})
  const handleChange = event => setUsername(event.target.value)

  useEffect(() => {
    if (username && username !== availability.username) {
      // The promises returned by getUsernameAvailability
      // will always resolve in the same order as they're created,
      // so we don't have to worry about race conditions here.
      getUsernameAvailability(username).then(
        isAvailable => setAvailability({ username, isAvailable })
      )
    }
  })

  return (
    <div className="nametag">
      <h2>Hello</h2>
      <label>
        <p>my username is</p>
        <div className="field">
          <span className="at">@</span>
          <input value={username} onChange={handleChange} />
          <span className="icon">
            {username && availability.username === username && (
              availability.isAvailable ? "✔️" : "❌"
            )}
          </span>
        </div>
      </label>
    </div>
  )
}
Build In Progress

In the above example, the effect starts by checking whether validation still needs to be performed for the current username. Then, if validation is still required, it calls getUsernameAvailability() and saves the result to component state.

While it may seem like a minor (and imperfect) optimization, the if statement inside the above effect is actually incredibly important. Do you know why?

Without the if statement, you get an infinite loop.

Saving the validation result to component state triggers another update, which triggers the effect, which triggers the update, which… yeah, you get the idea.

Effects often need to update component state, and this presents a bit of a problem: if you run these effects in response to every update, you can easily end up with infinite loops. Typically though, you don’t want to run effects in response to every update. You only want them to respond to specific events — and that’s where the second argument of useEffect() comes in.

#Conditional effects

The second argument to useEffect() — called the dependencies array — instructs React to only run the effect if a dependency has changed from its previous value.

useEffect(
  () => {
    // do stuff after specified updates
  },
  [executeWhenThisChanges, orWhenThisChanges]
)

The dependencies array gives you a simple way to create effects that respond to specific events, without running into infinite loops. And with that in mind, let’s revisit the username example with an exercise.

Your task is to refactor the below example with a dependencies array, so that it no longer uses an if statement.

Be careful to add the dependencies array before removing the if statement though — otherwise you’ll end up with an infinite loop!

App.js
getUsernameAva … .js
styles.css
index.js
import React, { useEffect, useState } from 'react'
import getUsernameAvailability from './getUsernameAvailability'

export default function App() {
  const [username, setUsername] = useState('')
  const [availability, setAvailability] = useState({})
  const handleChange = event => setUsername(event.target.value)

  useEffect(() => {
    if (username && username !== availability.username) {
      // The promises returned by getUsernameAvailability
      // will always resolve in the same order as they're created,
      // so we don't have to worry about race conditions here.
      getUsernameAvailability(username).then(
        isAvailable => setAvailability({ username, isAvailable })
      )
    }
  })

  return (
    <div className="nametag">
      <h2>Hello</h2>
      <label>
        <p>my username is</p>
        <div className="field">
          <span className="at">@</span>
          <input value={username} onChange={handleChange} />
          <span className="icon">
            {username && availability.username === username && (
              availability.isAvailable ? "✔️" : "❌"
            )}
          </span>
        </div>
      </label>
    </div>
  )
}
Build In Progress

Did you have a go? Great! In that case, if you take a look at the solution for the above exercise, you might notice something interesting. Here’s the original:

useEffect(() => {
  if (username !== availability.username) {
    // ...
  }
})

In contrast, here’s the solution with a dependencies array:

useEffect(() => {
  if (username) {
    // ...
  }
}, [username])

At first, there might not seem to really be any point in making this change. Sure, it saved you a couple characters… but if you look a little closer, there’s something far more exciting going on. Do you know what it is? Have a little think about this before checking the box below.

Actually, there are two exciting things going on here!

First, there’s the fact that the number of validations is cut in half — you can confirm this by opening up the console in both of the above examples. This is because the effect is now only being run when the username has changed — as opposed to running when it differs to the last received username. It gives you a better result, despite using less code. Which brings me to the second exciting thing.

While the condition in the first snippet references two variables: username, and availability.username, the second snippet only needs the username. By using the dependencies array, you’ve eliminated the need for an extra piece of state!

The key feature of useEffect() is that it lets you specify not just what effect to run, but also when that effect should occur — and it lets you do this declaratively, without relying on component state at all! In fact, it actually takes this one step further: it lets you declare how the effect should end.

#The cleanup function

When you return a function from your useEffect() callback, React will call that function when the effect should no longer have any… effect. lol, sorry not sorry.

useEffect(
  () => {
    // do stuff after specified updates

    return () => {
      // finish doing stuff
    }
  },
  [executeWhenThisChanges, orWhenThisChanges]
)

This function is often called the cleanup function, and there are two situations in which React will call it. Do you know what they are? Have a think about it, and when you’re ready, check your answer below.

The cleanup function will be called:

  1. When the component is unmounted
  2. On the next time that the effect callback is run

The first of these situations is hopefully fairly obvious. But what about the second situation? This is what makes the effect hook so powerful. It lets you declare to React:

Yo React, I want this thing to happen, but only until the next time it happens (or until it no longer can happen).

The cleanup function is particularly important when dealing with data subscriptions — I’ll cover more on this in the routing section of React, Firebase & Bacon. For the moment though, let’s jump back to where we started: the animated tree.

In order to smooth out the tree’s animation, all you need to do is finish off the effect in the demo below. The thing is, there are actually two approaches that you can take. Do you know what they are? If so, try them out in the editor — and then check below to find out which one works best.

App.js
FractalTreeFrame.js
FractalHelpers.js
index.js
import React, { useEffect, useState } from 'react'
import FractalTreeFrame from './FractalTreeFrame'

// The height and width of the entire window
const { innerHeight, innerWidth } = window

export default function App() {
  const [time, setTime] = useState(Date.now())
  const [mousePosition, setMousePosition] = useState({
    x: innerWidth / 2,
    y: innerHeight / 2,
  })

  // useEffect(() => {
  //   window.requestAnimationFrame(() => {
  //     // Update time to trigger a re-render
  //     setTime(Date.now())
  //   })
  // })

  return (
    <FractalTreeFrame
      mousePosition={mousePosition}
      onMouseMove={({ clientX: x, clientY: y }) =>
        setMousePosition({ x, y })
      }
      time={time}
    />
  )
}
Build In Progress

Did you find both approaches?

#Approach 1: conditional effects

Given that time is only updated within requestAnimationFrame, you can limit the number of renders by adding time as a dependency.

useEffect(() => {
  window.requestAnimationFrame(() => {
    setTime(Date.now())
  })
}, [time])

So long as the tree is the top level component, this approach works fine and dandy. Be careful, though — if this component is not at the top level and is then unmounted inside a running application, it would cause a warning, as setTime would still be called after unmount.

#Approach 2: cleanup function

By calling cancelAnimationFrame in the cleanup function, you’ll ensure that any scheduled updates are cancelled when you move the mouse.

useEffect(() => {
  const frame = window.requestAnimationFrame(() => {
    setTime(Date.now())
  })
  return () => {
    window.cancelAnimationFrame(frame)
  }
})

I’d recommend taking this approach, for two reasons:

  1. It’ll work just as well if you decide to update time elsewhere
  2. It’ll work just as well if the component is mounted elsewhere

#The three rules

Phew, that was a lot of info on effects! It’s okay if you didn’t take it all in — as you keep using effects in your day-to-day (or in my React, Firebase & Bacon course), you’ll gradually build intuition for them. But to start with? All you need to remember is these three rules:

  1. useEffect() schedules an update after the DOM has been updated
  2. By passing a dependencies array, that update will only occur if a dependency has changed
  3. If you return a cleanup function, it’ll be called when the effect should should no longer apply

Got these three rules down? Good. Let’s see if we can’t put 'em all into practice with an exercise.

#Exercise: debounce the username form

If you take a look at the console for this example, you’ll see that as you type, a separate validation request is made for each and every keystroke. Given that at some level you’re going to be charged per request… well, if your app grows large enough, a little debounce could save a lot of bacon.

But what do I mean by debouncing? Simple: instead of validating each and every keystroke, you’ll want to delay validation until the user has stopped typing for a short period. Roughly speaking, here’s how to achieve this:

  1. When the user updates the input, you’ll use setTimeout() to schedule a validation to occur a few hundred milliseconds in the future.
  2. If the user updates the input again before the previously scheduled validation has occurred, then cancel it with clearTimeout() before scheduling another one.

Your task is to debounce the username input so that validation runs no more than once every 500ms.

You can check your work by opening up the console, and watching how many requests are made as you type. Then, once you’re happy with your work (or if you’re stuck but you’ve given the exercise a solid try), compare your work with my solution by clicking the solution button at the bottom left.

App.js
getUsernameAva … .js
styles.css
index.js
import React, { useEffect, useState } from 'react'
import getUsernameAvailability from './getUsernameAvailability'

export default function App() {
  const [username, setUsername] = useState('')
  const [availability, setAvailability] = useState({})

  useEffect(() => {
    if (username) {
      getUsernameAvailability(username).then(
        isAvailable => setAvailability({ username, isAvailable })
      )
    }
  }, [username])

  return (
    <div className="nametag">
      <h2>Hello</h2>
      <label>
        <p>my username is</p>
        <div className="field">
          <span className="at">@</span>
          <input
            value={username}
            onChange={event => setUsername(event.target.value)}
          />
          <span className="icon">
            {username && availability.username === username && (
              availability.isAvailable ? "✔️" : "❌"
            )}
          </span>
        </div>
      </label>
    </div>
  )
}
Build In Progress

    This article is based on the useEffect() lesson in React, Firebase & Bacon. If you found this lesson useful, then there’s plenty more demos, exercises and in-depth explanations where it came from. Check it out — I think you’ll love it! 🤓

    About James

    Hi! I've been playing with JavaScript for over half my life, and am building Frontend Armory to help share what I've learned along the way!

    Tokyo, Japan