DEV Community

Cover image for Using refs to check if a component is still mounted.
Tushar Kashyap
Tushar Kashyap

Posted on • Updated on

Using refs to check if a component is still mounted.

While reading Learning React by Alex Banks and Eve Porcello, I came across a very neat method to check if a component is still mounted or not.

This is useful because if you try to update the state of a component that is already unmounted you will get the following error.

memory leak error

To show you what I just said, let's start by making a simple React application which only has a single Card component and a button which mounts and unmounts that Card component.

Here is App.js

//App.js

import React, { useState } from "react";
import Card from "./Card";

export default function App() {
  const [showCard, setShowCard] = useState(true);

  const toggleCard = () => {
    setShowCard((prevState) => !prevState);
  };

  return (
    <>
      <button onClick={toggleCard}>Toggle Card</button>
      {showCard ? <Card /> : null}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

and Card.js

//Card.js

import React, { useState } from "react";

export default function Card() {
  const [creature, setCreature] = useState("Unicorn");

  const changeCreature = (e) => {
    e.persist();
    setTimeout(() => {
      setCreature(e.target.value);
    }, 3000);
  };

  return (
    <div className="card">
      <button onClick={changeCreature} value="Unicorn">
        Unicorn
      </button>
      <button onClick={changeCreature} value="Phoenix">
        Phoenix
      </button>
      <h1 className="card__heading">
        All about {creature === "Unicorn" ? "Unicorns" : "Phoenixes"}
      </h1>
      <p className="card__description">
        {creature === "Unicorn"
          ? "Often considered the most wondrous of all mythical creatures, the unicorn is also a symbol of magic, miracles, and enchantment. The magical and enchanting unicorn appears to only a rare few and has the ability to bestow magic, miracles and wisdom to those who are pure of heart and virtuous in their deeds."
          : "A phoenix is a mythological bird that cyclically regenerates or is otherwise born again. Associated with fire and the sun, a phoenix obtains new life by arising from the ashes of its predecessor."}
      </p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The content inside the Card component changes according to the state variable creature which can be changed using the two buttons Unicorn and Phoenix.

In the App component we have a state variable showCard, through which we can mount and unmount the Card component.

Please visit the Live Demo to see what I am exactly talking about, ignore the commented out code.

The Problem

Imagine we are getting the data about Unicorns and Phoenixes from an external API and it does actually take some time to retrieve that data if people are on a slow connection.

Here, I am trying to mimic the same behavior through a state change in creature which takes 3 seconds as I'm using the setTimeout() function which works asynchronously, implying that you can interact with the app throughout the setTimeout() duration which is exactly how asynchronous calls to an API work.

Now due to the slow connection, people may have a window of opportunity to press the Toggle Card button (which will unmount the Card) after they press any one of the Unicorn or Phoenix button which takes up 3 seconds to update the state (try it yourself in the sandbox).

This will reproduce the error I showed at the very beginning, the error says we are trying to update the state of a component that is already unmounted from our application which is exactly what we are doing, trying to update the state of our Card component which isn't mounted anymore.

The Fix

💡 We will be using a ref to check if the component is still mounted or not before we update the state.

If it was a real API call what would happen is we would still make the API call as soon as the button (Unicorn or Phoenix) is clicked but just before we update the state with the data we fetched, we would use a ref inside a conditional to check if the component we are updating the state of still exists or if it unmounted.

Looking at the code will make things clearer.

//Card.js

import React, { useEffect, useRef, useState } from "react";

export default function Card() {
  const [creature, setCreature] = useState("Unicorn");

  const mounted = useRef(false);

  useEffect(() => {
    mounted.current = true;
    return () => (mounted.current = false);
  });

  const changeCreature = (e) => {
    e.persist();
    setTimeout(() => {
      if (mounted.current) {
        setCreature(e.target.value);
      }
    }, 3000);
  };

  return (...);   //same as before
}
Enter fullscreen mode Exit fullscreen mode

As you can see here that the useEffect() we added runs after every re-render (as it has no dependencies) and sets mounted.current to be true every time. The actual magic ✨ happens in the cleanup function (the function we return) which runs only when the component unmounts and changes mounted.current to false.

In the function where I update the state I've included an if check to see if the component is still mounted before calling setCreature(), the instance when the Card component is not mounted to the screen mounted.current will equate to false and the state (i.e. creature) will never be updated preventing the memory leak error.

Go to the same Demo and uncomment the commented out code in Card.js to see for yourself.

Why use refs?

We use refs here because they're awesome 🔮. Yeah, that's the only reason.

The end gif

Okay I am kidding.

P.S. It's so hard to add humor, ughhh 😩. I know you must be shaking your head at the moment.

So why did we use a ref here instead of something like a state variable const [mounted, setMounted] = useState(false)?

The answer is pretty simple, updating a ref never causes a re-render, whereas updating the state (i.e. using setMounted()) obviously does which will cause the useEffect() to run again and again causing an infinite loop.

Taking it a step further

I see that checking if a component in mounted or not can be used at quite a lot of places so that is an opportunity to extract all of the logic inside a custom hook.

//useMountedRef.js

import { useRef, useEffect } from 'react';

export default function useMountedRef() {
  const mounted = useRef(false);

  useEffect(() => {
    mounted.current = true;
    return () => (mounted.current = false);
  });

  return mounted;
}
Enter fullscreen mode Exit fullscreen mode

Now it can be used anywhere as follows, const mounted = useMountedRef(); and checking with mounted.current before updating the state will prevent the memory leak error.

Remember: This is just a method of checking if a component is still mounted or not, the API request is still being made. So use it wisely if you can handle making that additional request.
Also, just use it anywhere where you feel the need for checking if a component is mounted.

Cover Illustration credits: Lukasz Adam

Top comments (16)

Collapse
 
ramblingenzyme profile image
Satvik Sharma

Not sure how I feel about this, but from my understanding, useEffect runs the cleanup function before each run of the effect, not just when the component updates. So, passing an empty array to the useEffect will make sure it's only set to false on unmount instead of flipping it between renders.

Collapse
 
blackr1234 profile image
blackr1234

I'm wondering why not an empty array too.

Collapse
 
soullivaneuh profile image
Sullivan SENECHAL

I did the same useMountedRef hook but with an empty array usage and it change not a thing. So yes, I would also prefer that way IMHO.

Collapse
 
soullivaneuh profile image
Sullivan SENECHAL • Edited

I am quite confused.

I did something like this (but with a local var instead), but I revert my change after finding this official article: reactjs.org/blog/2015/12/16/ismoun...

However, this article is talking about this.isMounted from a component class, not a hook.

My question is: Is it the same thing, and in this case also anti-pattern or what is the difference?

Thanks for the article!

Collapse
 
tusharkashyap63 profile image
Tushar Kashyap

See if you are able to cancel the request/promise in the clean up function of useEffect and check if it was cancelled before updating the state, that is basically the gist of the official React blog in the age of hooks.

"An optimal solution would be to find places where setState() might be called after a component has unmounted, and fix them."

This article does that exactly.

Any place you feel cancelling the process or request is not an option or too much work you can definitely use this, just a tool in your arsenal.

Collapse
 
borys_kupar profile image
Borys Kupar

Though this does a job, but it's still a workaround. I think the preferable way is to try to clean-up any side-effects when component is unmounted. In your example, setTimeout function actually returns timeout id, that you could use to do clearTimeout(timeoutId) on component unmount.

Collapse
 
tusharkashyap63 profile image
Tushar Kashyap • Edited

Yeah agreed, but the real deal is that we have a method to check if a component is unmounted, we can use it anywhere we feel the need to have this functionality.

Collapse
 
futureistaken profile image
R Z

Do you really think it's good to keep fetching data when the user has left the current page? The request should be aborted, not just suppressed.

Collapse
 
pallymore profile image
Yurui Zhang
Collapse
 
blackr1234 profile image
blackr1234 • Edited

This is a useful article too, thanks for the reference.

Collapse
 
tusharkashyap63 profile image
Tushar Kashyap • Edited

Here, the request is being made just one additional time and that too because a user got frustrated(this won't be happening for the majority of time)

Abort Controller isn't even supported by IE11 and I think a wasted API call isn't always as bad as a memory leak.

Collapse
 
karimkamel profile image
KarimKamel

what is the advantage of using "useRef.current" instead of just creating a variable "var isMounted = false(or true)"

Collapse
 
tusharkashyap63 profile image
Tushar Kashyap • Edited

I already answered this in a reply to another comment.

"I follow a general rule of using refs if I don't want to re-render, and using state if I want to re-render. I use local variables only for the derived state(incoming props). Also it was giving me a linting error as local variables value is lost on every re-render(which is fine in this case)."

Collapse
 
sankarrajendran profile image
Sankar-Rajendran

Why can't we use a plain variable like let isMounted = true and make it false at the unmount function( clean up). Useref is little overhead for this purpose right?

Collapse
 
tusharkashyap63 profile image
Tushar Kashyap

I follow a general rule of using refs if I don't want to re-render, and using state if I want to re-render. I use local variables only for the derived state(incoming props). Also it was giving me a linting error as local variables value is lost on every re-render(which is fine in this case).

Collapse
 
tusharkashyap63 profile image
Tushar Kashyap

This is why I love publishing to dev.to. It actually improves the article.