Checking if a component is unmounted using react hooks?
Asked Answered
B

8

23

I'm checking if a component is unmounted, in order to avoid calling state update functions.

  1. This is the first option, and it works
const ref = useRef(false)
  useEffect(() => {
    ref.current = true
    return () => {
      ref.current = false
    }
  }, [])

....
if (ref.current) {
  setAnswers(answers)
  setIsLoading(false)
}
....
  1. Second option is using useState, which isMounted is always false, though I changed it to true in component did mount
const [isMounted, setIsMounted] = useState(false)

useEffect(() => {
  setIsMounted(true)
  return () => {
    setIsMounted(false)
  }
}, [])

....
if (isMounted) {
  setAnswers(answers)
  setIsLoading(false)
}
....

Why is the second option not working compared with the first option?

Bikol answered 21/11, 2019 at 16:17 Comment(2)
This is the way to do it and here is the code for it. That is the same as your first example.Selfdeception
The second one doesn't work because when you set state when component unmounts but setting state will only show the changed value on next render and since you unmounted the component won't be re rendered.Selfdeception
C
39

I wrote this custom hook that can check if the component is mounted or not at the current time, useful if you have a long running operation and the component may be unmounted before it finishes and updates the UI state.

import { useCallback, useEffect, useRef } from "react";

export function useIsMounted() {
  const isMountedRef = useRef(true);
  const isMounted = useCallback(() => isMountedRef.current, []);

  useEffect(() => {
    return () => void (isMountedRef.current = false);
  }, []);

  return isMounted;
}

Usage

function MyComponent() {
  const [data, setData] = React.useState()
  const isMounted = useIsMounted()

  React.useEffect(() => {
    fetch().then((data) => {
      // at this point the component may already have been removed from the tree
      // so we need to check first before updating the component state
      if (isMounted()) {
        setData(data)
      }
    })
  }, [...])

  return (...)
}

Live Demo

Edit 58979309/checking-if-a-component-is-unmounted-using-react-hooks

Couvade answered 15/10, 2020 at 20:15 Comment(3)
Great solution imo. Am I the only one thinking this kinda thing should be either handled better in React or available in the library?Roesler
isMounted() will return false after one state update in MyComponent. To prevent this one should consider addding isMountedRef.current = true; in the useEffect of hook definition.Myo
isMounted should not be used, instead use isMountedRef directly. Otherwise you might still stuck in the react cycleGluteus
C
10

Please read this answer very carefully until the end.

It seems your component is rendering more than one time and thus the isMounted state will always become false because it doesn't run on every update. It just run once and on unmounted. So, you'll do pass the state in the second option array:

}, [isMounted])

Now, it watches the state and run the effect on every update. But why the first option works?

It's because you're using useRef and it's a synchronous unlike asynchronous useState. Read the docs about useRef again if you're unclear:

This works because useRef() creates a plain JavaScript object. The only difference between useRef() and creating a {current: ...} object yourself is that useRef will give you the same ref object on every render.


BTW, you do not need to clean up anything. Cleaning up the process is required for DOM changes, third-party api reflections, etc. But you don't need to habit on cleaning up the states. So, you can just use:

useEffect(() => {
    setIsMounted(true)
}, []) // you may watch isMounted state
     // if you're changing it's value from somewhere else

While you use the useRef hook, you are good to go with cleaning up process because it's related to dom changes.

Capitate answered 21/11, 2019 at 18:59 Comment(0)
C
4

If you want to use a small library for this, then react-tidy has a custom hook just for doing that called useIsMounted:

import React from 'react'
import {useIsMounted} from 'react-tidy'

function MyComponent() {
  const [data, setData] = React.useState(null)
  const isMounted = useIsMounted()
  React.useEffect(() => {
    fetchData().then((result) => {
      if (isMounted) {
        setData(result)
      }
    })
  }, [])
  // ...
}

Learn more about this hook

Disclaimer I am the writer of this library.

Complacent answered 8/10, 2020 at 13:57 Comment(0)
M
3

This is a typescript version of @Nearhuscarl's answer.

import { useCallback, useEffect, useRef } from "react";
/**
 * This hook provides a function that returns whether the component is still mounted.
 * This is useful as a check before calling set state operations which will generates
 * a warning when it is called when the component is unmounted.
 * @returns a function
 */
export function useMounted(): () => boolean {
  const mountedRef = useRef(false);
  useEffect(function useMountedEffect() {
      mountedRef.current = true;
      return function useMountedEffectCleanup() {
        mountedRef.current = false;
      };
    }, []);
  return useCallback(function isMounted() {
      return mountedRef.current;
    }, [mountedRef]);
}

This is the jest test

import { render, waitFor } from '@testing-library/react';
import React, { useEffect } from 'react';
import { delay } from '../delay';
import { useMounted } from "./useMounted";

describe("useMounted", () => {

  it("should work and not rerender", async () => {
    const callback = jest.fn();

    function MyComponent() {
      const isMounted = useMounted();

      useEffect(() => {
        callback(isMounted())
      }, [])

      return (<div data-testid="test">Hello world</div>);
    }

    const { unmount } = render(<MyComponent />)
    expect(callback.mock.calls).toEqual([[true]])
    unmount();
    expect(callback.mock.calls).toEqual([[true]])

  })

  it("should work and not rerender and unmount later", async () => {
    jest.useFakeTimers('modern');
    const callback = jest.fn();

    function MyComponent() {
      const isMounted = useMounted();

      useEffect(() => {
        (async () => {
          await delay(10000);
          callback(isMounted());
        })();
      }, [])

      return (<div data-testid="test">Hello world</div>);
    }

    const { unmount } = render(<MyComponent />)
    await waitFor(() => expect(callback).toBeCalledTimes(0));
    jest.advanceTimersByTime(5000);
    unmount();
    jest.advanceTimersByTime(5000);
    await waitFor(() => expect(callback).toBeCalledTimes(1));
    expect(callback.mock.calls).toEqual([[false]])
  })

})

Sources available in https://github.com/trajano/react-hooks-tests/tree/master/src/useMounted

Maestricht answered 14/1, 2022 at 20:31 Comment(0)
B
2

This cleared up my error message, setting a return in my useEffect cancels out the subscriptions and async tasks.

  import React from 'react'

  const MyComponent = () => {
    
   const [fooState, setFooState] = React.useState(null)

   React.useEffect(()=> {

    //Mounted
     getFetch()
        

    // Unmounted
     return () => {
        setFooState(false)
     }
   })

      return (
        <div>Stuff</div>
      )
    }

export {MyComponent as default}
Botsford answered 24/6, 2021 at 19:9 Comment(1)
don't think you should access state variable inside the unmountGluteus
R
1

Near Huscarl solution is good, but there is problem with using these hook with react router, because if you go from example news/1 to news/2 useRef value is set to false because of unmount, but value keep false. So you need init ref value to true on each mount.

import {useRef, useCallback, useEffect} from "react";

export function useIsMounted(): () => boolean {
  const isMountedRef = useRef(true);
  const isMounted = useCallback(() => isMountedRef.current, []);

  useEffect(() => {
    isMountedRef.current = true;
    return () => void (isMountedRef.current = false);
  }, []);

  return isMounted;
}
Raynard answered 14/6, 2022 at 13:56 Comment(0)
K
-1

It's hard to know without the larger context, but I don't think you even need to know whether something has been mounted. useEffect(() => {...}, []) is executed automatically upon mounting, and you can put whatever needs to wait until mounting inside that effect.

Kauffmann answered 21/11, 2019 at 16:43 Comment(1)
OP has missing code in ....... but since there is a setSometing in the if statement I assume it's to try to avoid setting state on a possibly unmounted component. Example hereSelfdeception
T
-1

For me, the best way looks like this:

useEffect(() => {
  let mounted = true;

  setTimeout(() => {
    if (mounted) {
      // do something
    }
  }, 1000);

  return () => {
    mounted = false;
  };
}, [lang]);
Trombone answered 17/5 at 7:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.