Side Effects and useEffect

Course: React JS & Tailwind CSS - Full Course

Introduction

Up until now, we’ve been working with state and rendering. That’s great for handling data that React knows about and can manage inside our app.

But what if we need to do something outside of React’s rendering process? Things like fetching data from an API, interacting with localStorage, or setting up even listeners. These are called side effects.

What is a Side Effect?

A side effect is any action your code performs that affects something outside of its own scope. In React, rendering should always stay pure, meaning that given the same state and props, your component always renders the same UI.

But side effects (like fetching data or setting up even listeners) break this purity because they reach outside the component. So to address this, React provides us with a special hook: useEffect.

What is useEffect?

useEffect is a React hook that lets you run side effects in your components. This is what the basic syntax for it looks like:

import { useEffect } from "react"

useEffect(() => {
  // Code you want to run
}, [])

This takes two arguments:

  1. A function - the code you want to run (your side effect).
  2. A dependency array - tells React when to run this effect.

We'll use some examples to illustrate the usefulness of useEffect.

Without useEffect

Let's take a look at what happens in React when you fetch data and store it in state without useEffect:

  const [joke, setJoke] = useState()

  fetch("https://official-joke-api.appspot.com/random_joke")
    .then((res) => res.json())
    .then((data) => setJoke(data))

  console.log(joke)

At first glance, this might seem fine - you fetch a joke, store it in state, and log it. But if you run this code, you'll notice your console quickly gets filled with jokes (before running into an error since this api only allows 100 calls per 15 minutes).

So why is this happening? The reason is because:

  1. React renders the component.
  2. The fetch runs immediately during rendering.
  3. The data comes back and setJoke(data) is called.
  4. Updating state causes the component to re-render.
  5. On re-render, the fetch function runs again… which calls setJoke again… which triggers another re-render… and on and on.

This causes an endless loop of renders and fetches, causing our app to slow down, and potentially even crash. To fix this, we'll need to wrap our API call in a useEffect.

Fetch With useEffect

  const [joke, setJoke] = useState()
  useEffect(() => {
    fetch("https://official-joke-api.appspot.com/random_joke")
      .then((res) => res.json())
      .then((data) => setJoke(data))
  }, [])

We'll leave the dependency array empty for now, so that it only runs once after rendering. We'll talk more about this later. For now let's just render the joke:

      {joke ? (
        <div>
          <p>{joke.setup}</p>
          <p>{joke.punchline}</p>
        </div>
      ) : (
        <p>Loading...</p>
      )}

Note: We use a ternary because joke is initially undefined when we use state. The fetch hasn't finished yet, so joke will still be undefined. React will throw an error if we try to read properties of undefined. So instead, we'll render some loading text, and once joke is set, it'll trigger a re-render and joke will no longer be undefined.

Dependency Array Explained

When using useEffect, you often see the second argument: the dependency array. This array controls when your effect should run.

If the array is empty, then the effect will only run once, after the first render. Inputting a value into the array will run the effect when the value changes.

Let's use an example to illustrate this:

import { useState, useEffect } from "react"

function App() {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState("")

  useEffect(() => {
    setMessage(`You clicked ${count} times`)
  }, [count]) 

  return (
    <div>
      <p>{message}</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

Essentially, what's happening is the effect only runs when count changes. The dependency array is telling React what variable to watch (count in this case). When we click the button, useEffect will see that count has changed, and so it runs the function to setMessage, and the message updates.

Wrap-Up

useEffect is the hook React provides for handling side effects, which are operations that happen outside of the normal rendering process, like fetching data, subscribing to events, or updating the DOM manually.

It runs after the component renders, keeping your UI responsive while performing these tasks. The dependency array controls when the effect runs - an empty array [] runs it only once, while including values [value] runs it whenever those values change.

Using useEffect helps prevent common issues like infinite loops and ensures your components behave predictably when working with asynchronous data or external resources.

Now that you've gotten the basics of useEffect down, you can finish building your task tracking app!