0

Please don't consider this a duplicate. I have searched many blogs and stuffs but haven't reached to the solution.

First of all, my question is 'how to resolve react state change in unmounted component error, and cancel an async function call that is imported within the useEffect hook?'

I know that you can resolve 'state changed in an unmounted component' error by using clean up function in useEffect. In the cases of async call within useEffect hook, you can do clearTimeout, use cancel token for Axios, create [isMounted] state to flag mounted/unmounted state, and etc. So, at least, I know how to veil the react warning

However, my boss wants not only veiling the warning but also cancelling the async request. Here's the simplified version of code.

import { fetchFunction } from '../../../somewhereInTheDir';

function Page() {
  const [data, setData] = useState([]);
  const [isMounted, setIsMounted] = useState(true);

  async function fetchData() {
    try {
      const data = await fetchFunction(someUri);

      setData(data);
    } catch (error) {
      console.warn(error);
    }
  }

  useEffect(() => {
    if(isMounted) {
      fetchData(ENDPOINT);
    }
    
    return () => {
      setIsMounted(false);
    }
  })
}

The isMounted state veils the warning. So I am seeing nothin in the console, which is a good sign. But this code does not block/cancel data coming from the endpoint when Page component is unmounted. And you cannot use strategies like putting cancel token in the http header, because fetchFunction is from outside.

ps. my boss said using isMounted state as if it were a JavaScript variable could be an anti-pattern from React, since React state change leads to a re-rendering. Is there a better way to veil the warning without using isMounted state?

Intaek
  • 315
  • 4
  • 15
  • Does this answer your question? https://stackoverflow.com/a/56443045/6463558 – Lin Du Apr 01 '22 at 05:02
  • If the issue is how to cancel the request without using the cancel token, you can look at these docs from Axios https://axios-http.com/docs/cancellation as it supports AbortController you can easily use that to cancel the requests. – J Singh Apr 01 '22 at 05:20
  • 1
    BTW: You are missing a dependency array in `useEffect`. This will cause the function to be called on every render, not just once. Also, you shouldn't really call a state function `setIsMounted(false)` in a cleanup handler. That makes no sense, the component is gone. – Qwerty Apr 01 '22 at 05:55
  • you need to implement cancel function on your own, see _J Singh_'s comment – bogdanoff Apr 01 '22 at 06:29

2 Answers2

1

You can try this approach instead. Keep track of the mounted status in the hook itself, as well as the conditional setState function.

Also it is important that you include the empty dependency array, otherwise your component will call the hook on every render.

import { fetchFunction } from '../../../somewhereInTheDir';

function Page() {
  const [data, setData] = useState([]);
  const [isMounted, setIsMounted] = useState(true);

  async function fetchData() {
    try {
      return await fetchFunction(someUri);
    } catch (error) {
      console.warn(error);
    }
  }

useEffect(() => { // <- you can't change this to async
  let mounted = true

    // Variant with Promise
    fetchData(ENDPOINT).then(data => {
      if (mounted) setData(data);
    })

    // Variant with async/await
    (async () => {
      const data = await fetchData(ENDPOINT)
      if (mounted) setData(data);
    }())
    
    return () => {
      mounted = false
    }
  }, []) // <- empty dependency array is important here
}

The following article introduces few other methods how to handle the mounting state even if you use multiple useEffect hooks, etc.

https://www.benmvp.com/blog/handling-async-react-component-effects-after-unmount/

Qwerty
  • 29,062
  • 22
  • 108
  • 136
  • Thank you Qwerty. I have implemented your solution and AbortController to solve the issue. I just have one more question though. In your code, inside useEffect, is there any way I can turn fetchData call into async await? Currently you are using callback but I prefer using async await method. – Intaek Apr 01 '22 at 07:29
  • 1
    @OrdinaryProbably Yes, you can easily rewrite Promise chain into async/await statements. You will just need to wrap it in an `async function` to allow use of `await` keyword, which might not look as clean as with Promises. I have updated the answer. Notice that async variant takes 1 additional line. *BTW, this is called IIFE* – Qwerty Apr 02 '22 at 02:00
0

Regarding the last paragraph of your question, you can use refs instead of state.

const isMounted = useRef(false)

useEffect(() => {
    isMounted.current = true
    return () => isMounted.current=false
}, [])
garritfra
  • 546
  • 4
  • 21
Erfan
  • 1,725
  • 1
  • 4
  • 12