84

I get this error:

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.

when fetching of data is started and component was unmounted, but function is trying to update state of unmounted component.

What is the best way to solve this?

CodePen example.

default function Test() {
    const [notSeenAmount, setNotSeenAmount] = useState(false)

    useEffect(() => {
        let timer = setInterval(updateNotSeenAmount, 2000) 

        return () => clearInterval(timer)
    }, [])

    async function updateNotSeenAmount() {
        let data // here i fetch data

        setNotSeenAmount(data) // here is problem. If component was unmounted, i get error.
    }

    async function anotherFunction() {
       updateNotSeenAmount() //it can trigger update too
    }

    return <button onClick={updateNotSeenAmount}>Push me</button> //update can be triggered manually
}
ZiiMakc
  • 31,187
  • 24
  • 65
  • 105
  • 2
    Your problem might be that you're using setInterval incorrectly, remove the function call: i.e. the `()` next to `updateNotSeenAmount` inside `setInterval`. – Khauri Jun 04 '19 at 11:08
  • @Khauri ye, fixed, but problem is still there. – ZiiMakc Jun 04 '19 at 11:11
  • I think the use of brackets is problematic here - you have proper cleanup code that cancels the timeout, so I think it would be safe to *remove* the `[]` being passed to `useEffect` – colinta Apr 14 '20 at 02:58

8 Answers8

132

The easiest solution is to use a local variable that keeps track of whether the component is mounted or not. This is a common pattern with the class based approach. Here is an example that implement it with hooks:

function Example() {
  const [text, setText] = React.useState("waiting...");

  React.useEffect(() => {
    let isCancelled = false;

    simulateSlowNetworkRequest().then(() => {
      if (!isCancelled) {
        setText("done!");
      }
    });

    return () => {
      isCancelled = true;
    };
  }, []);

  return <h2>{text}</h2>;
}

Here is an alternative with useRef (see below). Note that with a list of dependencies this solution won't work. The value of the ref will stay true after the first render. In that case the first solution is more appropriate.

function Example() {
  const isCancelled = React.useRef(false);
  const [text, setText] = React.useState("waiting...");

  React.useEffect(() => {
    fetch();

    return () => {
      isCancelled.current = true;
    };
  }, []);

  function fetch() {
    simulateSlowNetworkRequest().then(() => {
      if (!isCancelled.current) {
        setText("done!");
      }
    });
  }

  return <h2>{text}</h2>;
}

You can find more information about this pattern inside this article. Here is an issue inside the React project on GitHub that showcase this solution.

Samuel Vaillant
  • 3,667
  • 1
  • 14
  • 21
  • It's nice way, but how you can trigger update from outside? How to use it with interval? – ZiiMakc Jun 04 '19 at 11:37
  • I didn't understood the question, could you provide a bit more context? – Samuel Vaillant Jun 04 '19 at 11:38
  • @RTW you can't move your `updateNotSeenAmount` function inside `useEffect`? – Andrii Golubenko Jun 04 '19 at 11:41
  • @AndriiGolubenko i can, but i need it outside too. Added examples. – ZiiMakc Jun 04 '19 at 11:44
  • You can inline the function as @AndriiGolubenko suggested it. Another alternative is to use `useRef` rather than the local variable. With this solution the value is accessible everywhere inside the body of the component. – Samuel Vaillant Jun 04 '19 at 11:44
  • 1
    @RTW I've updated the answer with an alternative solution that use `useRef`. – Samuel Vaillant Jun 04 '19 at 11:49
  • The first answer is great, but the second part is not an idiomatic use of `useRef` – it only works here because `useEffect` has no dependencies. The same ref will persist across multiple mount/unmounts, and so the value `current` will only be `true` the first time the effect runs. Fine if you only want the `fetch` to fire once, but if `useEffect` is based on more values and runs many times, this could be problematic. – colinta Apr 14 '20 at 02:58
  • Not sure to understand your point. It looks like `useRef` doesn't persist across multiple mount/unmounts of a component. Take a look at this [example](https://codesandbox.io/s/react-useref-with-mountunmount-bn3cx). You can unmount the component before the request complete. Mount it again it will execute the request. – Samuel Vaillant Apr 14 '20 at 15:39
  • You're right on the dependencies part though. The value will stay true if one of the dependency is updated without an unmount of the component. With dependencies the first solution is more appropriate. I've updated the anwser. – Samuel Vaillant Apr 14 '20 at 15:53
  • It seems that this approach is anti-pattern as it says here, https://stackoverflow.com/questions/56442582/react-hooks-cant-perform-a-react-state-update-on-an-unmounted-component, am I right?@SamuelVaillant – karianpour May 01 '20 at 13:06
  • @karianpour I think you provided the wrong link. Your link refers to the same question above. But I agree that the proposed solution is an anti-pattern, which pollutes the codebase with unnecessary boiler-plate code just to avoid an occasional warning. It does not actually solve any problems. You can choose to call `setState` after checking the boolean (is the component unmounted) or you can let React do the same internally and issue a warning in case it is true. The amount of code and complexity added is not proportional to the benefits. – Snackoverflow May 05 '20 at 10:44
  • @anddero, sorry for the link. I had the problem in async call which some how caused react to unmound the componemt. I could solve the problem by shifting the logic, which caused the unmount, to be done later by `setTimeout(logic, 0)`. Thanks. – karianpour May 07 '20 at 09:37
  • 2nd approach (useRef) solved my problem however I wonder why I have to do this plumbing when React could do it automatically. State changer functions are created by React useState, why React can't disable them when component is unmounted, or give me just one function like disableAllStateUpdate I can call in the unmount? Why I have to add if (!isCancelled.current) to each state changer? Maybe I'm missing something (?) Would like to hear others opinions about that. Cheers ;) – Anton M May 27 '20 at 10:55
  • Kudos for the reference links to the article and to the associated GitHub issue! Cheers – Glenn Mohammad Feb 14 '22 at 18:09
5

If you are fetching data from axios(using hooks) and the error still occurs, just wrap the setter inside the condition

let isRendered = useRef(false);
useEffect(() => {
    isRendered = true;
    axios
        .get("/sample/api")
        .then(res => {
            if (isRendered) {
                setState(res.data);
            }
            return null;
        })
        .catch(err => console.log(err));
    return () => {
        isRendered = false;
    };
}, []);
Drew Cordano
  • 962
  • 9
  • 16
4

TL;DR

Here is a CodeSandBox example

The other answers work of course, I just wanted to share a solution I came up with. I built this hook that works just like React's useState, but will only setState if the component is mounted. I find it more elegant because you don't have to mess arround with an isMounted variable in your component !

Installation :

npm install use-state-if-mounted

Usage :

const [count, setCount] = useStateIfMounted(0);

You can find more advanced documentation on the npm page of the hook.

Nans .D
  • 105
  • 1
  • 2
1

Here is a simple solution for this. This warning is due to when we do some fetch request while that request is in the background (because some requests take some time.)and we navigate back from that screen then react cannot update the state. here is the example code for this. write this line before every state Update.

if(!isScreenMounted.current) return;

Here is Complete Example

import React , {useRef} from 'react'
import { Text,StatusBar,SafeAreaView,ScrollView, StyleSheet } from 'react-native'
import BASEURL from '../constants/BaseURL';
const SearchScreen = () => {
    const isScreenMounted = useRef(true)
    useEffect(() => {
        return () =>  isScreenMounted.current = false
    },[])

    const ConvertFileSubmit = () => {
        if(!isScreenMounted.current) return;
         setUpLoading(true)
 
         var formdata = new FormData();
         var file = {
             uri: `file://${route.params.selectedfiles[0].uri}`,
             type:`${route.params.selectedfiles[0].minetype}`,
             name:`${route.params.selectedfiles[0].displayname}`,
         };
         
         formdata.append("file",file);
         
         fetch(`${BASEURL}/UploadFile`, {
             method: 'POST',
             body: formdata,
             redirect: 'manual'
         }).then(response => response.json())
         .then(result => {
             if(!isScreenMounted.current) return;
             setUpLoading(false)    
         }).catch(error => {
             console.log('error', error)
         });
     }

    return(
    <>
        <StatusBar barStyle="dark-content" />
        <SafeAreaView>
            <ScrollView
            contentInsetAdjustmentBehavior="automatic"
            style={styles.scrollView}>
               <Text>Search Screen</Text>
            </ScrollView>
        </SafeAreaView>
    </>
    )
}

export default SearchScreen;


const styles = StyleSheet.create({
    scrollView: {
        backgroundColor:"red",
    },
    container:{
        flex:1,
        justifyContent:"center",
        alignItems:"center"
    }
})
Engr.Aftab Ufaq
  • 3,356
  • 3
  • 21
  • 47
0

This answer is not related to the specific question but I got the same 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. and as a React newcomer could not find a solution to it.

My problem was related to useState in an unmounted component.

I noticed that I was calling a set state function (setIsLoading) after the function that unmounted my component:

const Login = () => {
  const [isLoading, setIsLoading] = useState(false);

  const handleLogin = () => {
    setIsLoading(true);
    firebase.auth().then(      
      functionToUnMountLoginSection();
      // the problem is here
      setIsLoading(false);
    )
  }
}

The correct way is to call setIsLoading when the component is still mounted, before calling the function to unmount/process user login in my specific case:

    firebase.auth().then(     
      setIsLoading(false);
      functionToUnMountLoginSection();      
    )
Nico Serrano
  • 577
  • 4
  • 14
0

You add the state related datas into the useEffect body for not rerunning them every rerendering process. This method will solve the problem.

useEffect(() => {
    let timer = setInterval(updateNotSeenAmount, 2000) 

    return () => clearInterval(timer)
}, [notSeenAmount])

REF: Tip: Optimizing Performance by Skipping Effects

Hasan Basri
  • 848
  • 8
  • 16
0

Custom Hook Solution (ReactJs/NextJs)

Create a new folder named 'shared' and add two folders named 'hooks', 'utils' in it. Add a new file called 'commonFunctions.js' inside utils folder and add the code snippet below.

export const promisify = (fn) => {
    return new Promise((resolve, reject) => {
        fn
            .then(response => resolve(response))
            .catch(error => reject(error));
    });
};

Add a new file called 'fetch-hook.js' inside hooks folder and add the code snippet below.

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

import { promisify } from "../utils/commonFunctions";

export const useFetch = () => {
    const isUnmounted = useRef(false);

    useEffect(() => {
        isUnmounted.current = false;

        return () => {
            isUnmounted.current = true;
        };
    }, []);

    const call = useCallback((fn, onSuccess, onError = null) => {
        promisify(fn).then(response => {
            console.group('useFetch Hook response', response);

            if (!isUnmounted.current) {
                console.log('updating state..');

                onSuccess(response.data);
            }
            else
                console.log('aborted state update!');

            console.groupEnd();
        }).catch(error => {
            console.log("useFetch Hook error", error);

            if (!isUnmounted.current)
                if (onError)
                    onError(error);
        });
    }, []);

    return { call }
};

Folder Structure

enter image description here

Our custom hook is now ready. We use it in our component like below

const OurComponent = (props) => {
  //..
  const [subscriptions, setSubscriptions] = useState<any>([]);
  //..
  const { call } = useFetch();

  // example method, change with your own
  const getSubscriptions = useCallback(async () => {
    call(
      payment.companySubscriptions(userId), // example api call, change with your own
      (data) => setSubscriptions(data),
    );
  }, [userId]);
  //..
  const updateSubscriptions = useCallback(async () => {
    setTimeout(async () => {
      await getSubscriptions();
    }, 5000);// 5 seconds delay
  }, [getSubscriptions]);
  //..
}

In our component, we call 'updateSubscriptions' method. It will trigger 'getSubscriptions' method in which we used our custom hook. If we try to navigate to a different page after calling updateSubscriptions method before 5 seconds over, our custom hook will abort state update and prevent that warning on the title of this question

enter image description here

Wanna see opposite?

Change 'getSubscriptions' method with the one below

const getSubscriptions = useCallback(async () => {
  const response = await payment.companySubscriptions(userId);

  setSubscriptions(response);
}, [userId]);

Now try to call 'updateSubscriptions' method and navigate to a different page before 5 seconds over

enter image description here

Ahmet Firat Keler
  • 2,603
  • 2
  • 11
  • 22
0

Try this custom hook:

import { useEffect, useRef } from 'react';

export const useIsMounted = () => {
  const isMounted = useRef(false);
  useEffect(() => {
    isMounted.current = true;
    return () => (isMounted.current = false);
  }, []);
  return isMounted;
};

function Example() {
  const isMounted = useIsMounted();
  const [text, setText] = useState();
  const safeSetState = useCallback((callback, ...args) => {
    if (isMounted.current) {
      callback(...args);
    }
  }, []);

    useEffect(() => {
        safeSetState(setText, 'Hello')
      });
    }, []);

  return <h2>{text}</h2>;
}
DungGramer
  • 177
  • 10