React: Prevent state updates on unmounted components
Tags: react
Published: 2022-07-22 12:40:12

You might have seen the following warning randomly appearing in your browser console, whenever you are debugging your React app:

`Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.`

![unmounted warning](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z54b1i34tcta3rkow1dv.png) 

Ever wondered why this happens?

This happens in the following scenario:

- You make an asynchronous call (eg: Network call) inside a component.
- The component which made the call gets unmounted due to some user action (eg: user navigating away).
- The asynchronous call responds and you have `setState` call in the success handler.

In the above case, React tries to set the state of an unmounted component, which is not necessary since the component is not in scope anymore. Hence, React warns us that there is a piece of code that tries to update the state of an unmounted component. As React suggests, this will not introduce any bugs in the application, however, it might use up unnecessary memory.

In this article, we will see different scenarios, where this error can occur, and how we can fix them.

## Fetch calls

Consider the following code:

```jsx
import { useEffect, useState } from "react"

const FetchPosts = () => {
  const [posts, setPosts] = useState([])
  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(
          "https://jsonplaceholder.typicode.com/posts"
        )
        console.log("received response")
        const data = await response.json()
        setPosts(data)
      } catch (e) {
        console.log(e)
      }
    }

    fetchData()
  }, [])
  return (
    <ul>
      {posts.map(post => {
        return <li key={post.id}>{post.title}</li>
      })}
    </ul>
  )
}

export default FetchPosts
```

Here, when the component is mounted, we are calling the [JSON Placeholder](https://jsonplaceholder.typicode.com/) API and displaying the posts in a list.

Now include the component in the `App` component:

```jsx
import React, { useState } from "react"
import FetchPosts from "./FetchPosts"

function App() {
  const [showPosts, setShowPosts] = useState()

  return (
    <div>
      <button onClick={() => setShowPosts(true)}>Fetch Posts</button>
      <button onClick={() => setShowPosts(false)}>Hide Posts</button>
      {showPosts && <FetchPosts />}
    </div>
  )
}

export default App
```

Now if you run the code and click on 'Fetch Posts' and then click on 'Hide Posts' immediately, even before the response is received, you will see the message being logged (even though the component is unmounted) and a warning in the console:

![fetch warning](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cb7uk4weds88kimro1jp.png) 

You can set the throttling to Slow 3G if the response comes quickly and you are unable to click on 'Hide Posts' on time.

![network throttling](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mg2f7pm8ngizx25snoly.png)

How to solve this warning?

There is an interface called [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController), which helps in cancelling web requests whenever user needs to.

```jsx
import { useEffect, useState } from "react"

const FetchPosts = () => {
  const [posts, setPosts] = useState([])
  useEffect(() => {
    const controller = new AbortController()
    const signal = controller.signal
    const fetchData = async () => {
      try {
        const response = await fetch(
          "https://jsonplaceholder.typicode.com/posts",
          {
            signal: signal,
          }
        )
        console.log("received response")
        const data = await response.json()
        setPosts(data)
      } catch (e) {
        console.log(e)
      }
    }

    fetchData()

    return () => {
      controller.abort()
    }
  }, [])
  return (
    <ul>
      {posts.map(post => {
        return <li key={post.id}>{post.title}</li>
      })}
    </ul>
  )
}

export default FetchPosts
```

As you can see in the above code, we access the [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) and pass it to the fetch request. Whenever the component is unmounted, we will be aborting the request (in the return callback of `useEffect`).

## Axios calls

Let's rewrite the `FetchPosts` component to make use of [axios](https://www.codingdeft.com/posts/react-fetch-data-api/#fetching-data-in-react-using-axios).

Make sure that you have installed axios using the following command (or use `npm i axios`):

```bash
yarn add axios
```

Now use it in the `AxiosPosts` component:

```jsx
import axios from "axios"
import { useEffect, useState } from "react"

export const AxiosPosts = () => {
  const [posts, setPosts] = useState([])
  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await axios.get(
          "https://jsonplaceholder.typicode.com/posts"
        )
        console.log("received response")
        const data = response.data
        setPosts(data)
      } catch (e) {
        console.log(e)
      }
    }

    fetchData()
  }, [])
  return (
    <ul>
      {posts.map(post => {
        return <li key={post.id}>{post.title}</li>
      })}
    </ul>
  )
}

export default AxiosPosts
```

Now, if you include `AxiosPosts` in the App component and click on 'Fetch Posts' and 'Hide Posts' before the response is received, you will see the warning.

To cancel previous requests in React, axios has something called [CancelToken](https://axios-http.com/docs/cancellation#cancel-token-code-deprecated-code). In my [previous article](https://www.codingdeft.com/posts/axios-cancel-previous-request-react/), I have explained in detail how to cancel previous requests in axios. We will make use of the same logic here.

```jsx
import axios from "axios"
import { useEffect, useState } from "react"

export const AxiosPosts = () => {
  const [posts, setPosts] = useState([])
  useEffect(() => {
    let cancelToken

    const fetchData = async () => {
      cancelToken = axios.CancelToken.source()
      try {
        const response = await axios.get(
          "https://jsonplaceholder.typicode.com/posts",
          { cancelToken: cancelToken.token }
        )
        console.log("received response")
        const data = response.data
        setPosts(data)
      } catch (e) {
        console.log(e)
      }
    }

    fetchData()

    return () => {
      cancelToken.cancel("Operation canceled.")
    }
  }, [])
  return (
    <ul>
      {posts.map(post => {
        return <li key={post.id}>{post.title}</li>
      })}
    </ul>
  )
}

export default AxiosPosts
```

As of axios `v0.22.0`, [CancelToken](https://axios-http.com/docs/cancellation#cancel-token-code-deprecated-code) is deprecated and axios recommends to use `AbortController` like we used in `fetch` calls. This is how the code would look like if we are making use of `AbortController`:

```jsx
import axios from "axios"
import { useEffect, useState } from "react"

export const AxiosPosts = () => {
  const [posts, setPosts] = useState([])
  useEffect(() => {
    const controller = new AbortController()
    const signal = controller.signal

    const fetchData = async () => {
      try {
        const response = await axios.get(
          "https://jsonplaceholder.typicode.com/posts",
          {
            signal: signal,
          }
        )
        console.log("received response")
        const data = response.data
        setPosts(data)
      } catch (e) {
        console.log(e)
      }
    }

    fetchData()

    return () => {
      controller.abort()
    }
  }, [])
  return (
    <ul>
      {posts.map(post => {
        return <li key={post.id}>{post.title}</li>
      })}
    </ul>
  )
}

export default AxiosPosts
```

## setTimeout calls

[setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout) is another asynchronous call where we would encounter this warning.

Consider the following component:

```jsx
import React, { useEffect, useState } from "react"

const Timer = () => {
  const [message, setMessage] = useState("Timer Running")
  useEffect(() => {
    setTimeout(() => {
      setMessage("Times Up!")
    }, 5000)
  }, [])
  return <div>{message}</div>
}

const Timeout = () => {
  const [showTimer, setShowTimer] = useState(false)
  return (
    <div>
      <button onClick={() => setShowTimer(!showTimer)}>Toggle Timer</button>
      <div>{showTimer && <Timer />}</div>
    </div>
  )
}

export default Timeout
```

Here we have a state having an initial value of 'Timer Running', which will be set to 'Times Up!' after 5 seconds. If you toggle the timer before the timeout happens, you will get the warning.

We can fix this by calling [clearTimeout](https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout) on the timeout ID returned by the `setTimeout` call, as shown below:

```jsx
import React, { useEffect, useRef, useState } from "react"

const Timer = () => {
  const [message, setMessage] = useState("Timer Running")
  // reference used so that it does not change across renders
  let timeoutID = useRef(null)
  useEffect(() => {
    timeoutID.current = setTimeout(() => {
      setMessage("Times Up!")
    }, 5000)

    return () => {
      clearTimeout(timeoutID.current)
      console.log("timeout cleared")
    }
  }, [])
  return <div>{message}</div>
}

const Timeout = () => {
  const [showTimer, setShowTimer] = useState(false)
  return (
    <div>
      <button onClick={() => setShowTimer(!showTimer)}>Toggle Timer</button>
      <div>{showTimer && <Timer />}</div>
    </div>
  )
}

export default Timeout
```

## setInterval calls

Similar to setTimeout, we can fix the warning by calling [clearInterval](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval) whenever the `useEffect` cleanup function is called:

```jsx
import React, { useEffect, useRef, useState } from "react"

const CountDown = () => {
  const [remaining, setRemaining] = useState(10)
  // reference used so that it does not change across renders
  let intervalID = useRef(null)
  useEffect(() => {
    if (!intervalID.current) {
      intervalID.current = setInterval(() => {
        console.log("interval")
        setRemaining(existingValue =>
          existingValue > 0 ? existingValue - 1 : existingValue
        )
      }, 1000)
    }
    return () => {
      clearInterval(intervalID.current)
    }
  }, [])
  return <div>Time Left: {remaining}s</div>
}

const Interval = () => {
  const [showTimer, setShowTimer] = useState(false)
  return (
    <div>
      <button onClick={() => setShowTimer(!showTimer)}>Toggle Timer</button>
      <div>{showTimer && <CountDown />}</div>
    </div>
  )
}

export default Interval
```

## Event listeners

Event listeners is another example of asynchronous calls. Say there is a box and you want to identify if the user has clicked inside or outside the box. Then as I described in [one of my previous articles](https://www.codingdeft.com/posts/react-on-click-outside/), we will bind an onClick listener to the document and check if the click is triggered within the box or not:

```jsx
import React, { useEffect, useRef, useState } from "react"

const Box = () => {
  const ref = useRef(null)
  const [position, setPosition] = useState("")

  useEffect(() => {
    const checkIfClickedOutside = e => {
      if (ref.current && ref.current.contains(e.target)) {
        setPosition("inside")
      } else {
        setPosition("outside")
      }
    }
    document.addEventListener("click", checkIfClickedOutside)
  }, [])

  return (
    <>
      <div>{position ? `Clicked ${position}` : "Click somewhere"}</div>
      <div
        ref={ref}
        style={{
          width: "200px",
          height: "200px",
          border: "solid 1px",
        }}
      ></div>
    </>
  )
}

const DocumentClick = () => {
  const [showBox, setShowBox] = useState(false)
  return (
    <>
      <div
        style={{
          display: "flex",
          justifyContent: "center",
          alignItems: "center",
          flexDirection: "column",
          height: "100vh",
        }}
      >
        <button
          style={{ marginBottom: "1rem" }}
          onClick={() => setShowBox(!showBox)}
        >
          Toggle Box
        </button>
        {showBox && <Box />}
      </div>
    </>
  )
}

export default DocumentClick
```

Now if you click on 'Toggle Box', a box will be shown. If you click anywhere, the message will change based on where you have clicked. If you hide the box now by clicking on the 'Toggle Box' and click anywhere in the document, you will see the warning in the console.

You can fix this by calling [removeEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener) during the `useEffect` cleanup:

```jsx
import React, { useEffect, useRef, useState } from "react"

const Box = () => {
  const ref = useRef(null)
  const [position, setPosition] = useState("")

  useEffect(() => {
    const checkIfClickedOutside = e => {
      if (ref.current && ref.current.contains(e.target)) {
        setPosition("inside")
      } else {
        setPosition("outside")
      }
    }
    document.addEventListener("click", checkIfClickedOutside)
    return () => {
      document.removeEventListener(checkIfClickedOutside)
    }
  }, [])

  return (
    <>
      <div>{position ? `Clicked ${position}` : "Click somewhere"}</div>
      <div
        ref={ref}
        style={{
          width: "200px",
          height: "200px",
          border: "solid 1px",
        }}
      ></div>
    </>
  )
}

const DocumentClick = () => {
  const [showBox, setShowBox] = useState(false)
  return (
    <>
      <div
        style={{
          display: "flex",
          justifyContent: "center",
          alignItems: "center",
          flexDirection: "column",
          height: "100vh",
        }}
      >
        <button
          style={{ marginBottom: "1rem" }}
          onClick={() => setShowBox(!showBox)}
        >
          Toggle Box
        </button>
        {showBox && <Box />}
      </div>
    </>
  )
}

export default DocumentClick
```

## Source Code

You can view the complete source code [here](https://github.com/collegewap/react-prevent-state-update-unmounted).