react hook cant perform setstate in an unmounted component - reactjs

const SideAd = () => {
const [sideAd, setSideAd] = useState([])
useEffect(() => {
query()
}, [])
const query = async () => {
const res = await getSideAd()
Array.isArray(res) && setSideAd(res)
}
return (
<div className="sideAdComponent">
</div>
)
}
I just want make a request at mounted and then set a new state. But instead I got error react hook cant perform setstate in an unmounted component

There are 2 things you may do here.
First is fighting race conditions in useEffect with using cleanup function(return value in useEffect's callback)
const SideAd = () => {
const [sideAd, setSideAd] = useState([])
useEffect(() => {
const isMounted = true;
getSideAd().then(res => Array.isArray && isMounted && setSideAd(res))
return () => {isMounted = false;}
}, [])
return (
<div className="sideAdComponent">
</div>
)
}
Check nice article at hackernoon for more detailed explanation. To my mind this approach is even better then cancelling request(because it does not require you modify requester's code).
Another thing: maybe it's better to check for a reason why your component unmounted before response comes. Probably there is legit reason for that(say, you've navigated away while response has not come yet).
But it's also possible there is some HOC that declares component constructor inline and that causes all subtree to be re-created instead of updating. So it's worth spending some time on investigation - because there may be hidden bugs you have not found yet(example).

You can write a custom hook called useIsMounted:
const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => isMounted.current = false;
}, []);
return isMounted;
};
const SideAd = () => {
const [sideAd, setSideAd] = useState([]);
useEffect(() => {
query();
}, []);
const mounted = useIsMounted();
const query = async () => {
const res = await getSideAd();
Array.isArray(res) && mounted.current && setSideAd(res);
};
return <div className="sideAdComponent"></div>;
};

Related

Unmounted component - React Native

I have to functions/const to get data from API:
const [isLoadingRoom, setLoadingRoom] = useState(true);
const [isLoadingLobby, setLoadingLobby] = useState(true);
const [rooms, setRooms] = useState([]);
const [lobbies, setLobbies] = useState([]);
const getRooms = async () => {
let isMounted = true;
async function fetchData() {
const response = await fetch(link);
const json = await response.json();
// 👇️ only update state if component is mounted
if (isMounted) {
setRooms(json);
setLoadingRoom(false);
}
}
fetchData();
return () => {
isMounted = false;
}
}
const getLobbies = async () => {
let isMounted = true;
async function fetchData() {
const response = await fetch(link);
const json = await response.json();
// 👇️ only update state if component is mounted
if (isMounted) {
setLobbies(json);
setLoadingLobby(false);
}
}
fetchData();
return () => {
isMounted = false;
}
}
useEffect(() => {
const roomInterval = setInterval(() => {
getRooms();
getLobbies();
}, 5000);
return () => clearInterval(roomInterval);
}, []);
The API gets data every 5 second, but after a while I get this message:
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.
I have tried different approaches to fetch the API with const, functions, async etc. but I get this error message anyway.. Any tips?
useRef rather than normal variable:
const isMountedRef = useRef(true);
useEffect(() => {
const roomInterval = setInterval(() => {
getRooms();
getLobbies();
}, 5000);
return () => {
clearInterval(roomInterval);
isMountedRef.current = false;
};
}, []);
and change check conditions to
if(isMountedRef.current){
// execute setState
}
Hope it helps. feel free for doubts

How to ignore previous async effects when useEffect is called again?

I have a simple component that makes an async request when some state changes:
const MyComp = () => {
const [state, setState] = useState();
const [result, setResult] = useState();
useEffect(() => {
fetchResult(state).then(setResult);
}, [state]);
return (
<div>{result}</div>
);
};
The problem is, sometimes the state changes twice in a short lapse of time, and the fetchResult function can take a very different amount of time to resolve according to the state value, so sometimes this happens:
As you can guess, as state now is state2 and not state1 anymore, I would like result to be result2, ignoring the response received in the then of the -obsolete- first effect call.
Is there any clean way to do so?
I would suggest you setup some kind of request cancellation method in the useEffect cleanup function.
For example with axios, it looks like that:
const MyComp = () => {
const [state, setState] = useState();
const [result, setResult] = useState();
useEffect(() => {
const source = axios.CancelToken.source();
fetchResult({state, cancelToken: source.cancelToken }).then(setResult);
return () => {
source.cancel()
}
}, [state]);
return (
<div>{result}</div>
);
};
You have a similar API with fetch called AbortController
What this will do is it will cancel the stale requests if your state changed so only the last one will resolve (and set result).
I've not tested this... but my initial thought would be if you have the state in the response, you could check if the state fetched matches the current state. If not, then the state has changed since the request and you no longer care about the response so don't set it.
useEffect(() => {
fetchResult(state).then((response) => {
response.state === state ? setResult(response.data) : false;
});
}, [state]);
You might also be able to do it by keeping a record of the fetchedState on each request.. and again discard it if it no longer matches.
useEffect(() => {
let fetchedState = state;
fetchResult(fetchedState).then((response) => {
fetchedState === state ? setResult(response) : false;
});
}, [state]);
I've built something like the below in order to only ever use the last result of the last request sent:
const REQUEST_INTERVAL = 2000
const MyComponent = () => {
const [inputState, setInputState] = useState();
const [result, setResult = useState()
const requestIndex = useRef(0)
useEffect(() => {
const thisEffectsRequestIndex = requestIndex.current + 1
requestIndex.current = thisEffectsRequestIndex
setTimeout(() => {
if(thisEffectsRequestIndex === requestIndex.current) {
fetch('http://example.com/movies.json')
.then((response) => {
if(thisEffectsRequestIndex === requestIndex.current) {
setResult(response.json())
}
})
}
})
, REQUEST_INTERVAL)
}, [inputState])
return <div>{result}</div>
}

React async event handler. The safe way

Imagine a simple react component
const Upload: React.FC = () => {
const [done, setDone] = useState(false)
const upload = async () => {
await doSomeAsyncStuffHere()
setDone(true)
}
if(done) {
return <div>success</div>
}
return (
<button onClick={upload}>upload</button>
)
}
It looks fine at first glance. But what if upload function takes a long time to finish? What if user navigates to another view and the component gets unmounted? When the async task finishes will cause a state update on an unmounted component. This is a no-op and a possible memory leak. What should I do to prevent it?
One way of going about it is to create a ref that you set to false when the component is unmounted, and check against this before setting the result of your asynchronous code in the component state.
Example
const Upload: React.FC = () => {
const isMounted = useRef(true);
const [done, setDone] = useState(false)
const upload = async () => {
await doSomeAsyncStuffHere()
if (isMounted.current) {
setDone(true)
}
}
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
if(done) {
return <div>success</div>
}
return (
<button onClick={upload}>upload</button>
)
}

Stop useEffect from running on mount

I only want useEffect to run when my dependency list changes, it is also running every time the component is mounted, is there any way to not fire on mount?
You can tell React to skip applying an effect if certain values
haven’t changed between re-renders.
I initially thought that meant it shouldn't re-render on subsequent mounts but this question cleared that up.
I am displaying a list of records in a master "page" (react-router), the user can choose a single record and go to the detail page, and then return to the master page - so the master list component is completely unmounted/mounted in that scenario. And every time I load the "master page", I see the data being fetched, I only want this to happen when one of the dependencies changes; these dependencies and the data itself are stored in Redux so they're global.
Can useEffect or another hook be made to only fire when the dependencies change?
const {page, pageSize, search, sorts} = useSelector(getFilters);
const data = useSelector(getData);
useEffect(() => {
console.log("fetching");
dispatch(fetchData(page, pageSize, search, sorts));
}, [page, pageSize, search, sorts]);
You can't configure it out of the box.
But, a common pattern is to use some isMounted flag like so:
// Is Mounted
const useFetchNotOnMount = () => {
...
const isMounted = useRef(false);
useEffect(() => {
if (isMounted.current) {
console.log('fetching');
dispatch(fetchData(filters));
} else {
isMounted.current = true;
}
}, [dispatch, filters]);
};
// Same (Is First Render)
const useFetchNotOnMount = () => {
...
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
} else {
console.log("fetching");
dispatch(fetchData(filters));
}
}, [dispatch, filters]);
};
Read more in depth uses of useEffect
If you have several useEffect to prevent from running at initially, you can do the following:
export default function App() {
const mountedRef = useMountedRef();
const [isLoggedIn, setLoggedIn] = React.useState(false);
const [anotherOne, setAnotherOne] = React.useState(false);
React.useEffect(() => {
if (mountedRef.current) {
console.log("triggered", isLoggedIn);
}
}, [isLoggedIn]);
React.useEffect(() => {
if (mountedRef.current) {
console.log("triggered", anotherOne);
}
}, [anotherOne]);
React.useEffect(() => {
if (mountedRef.current) {
console.log("triggered", isLoggedIn, anotherOne);
}
}, [anotherOne, isLoggedIn]);
return (
<div>
<button onClick={() => setLoggedIn(true)}>Login</button>
</div>
);
}
const useMountedRef = () => {
const mountedRef = React.useRef(false);
React.useEffect(() => {
setTimeout(() => {
mountedRef.current = true;
});
}, []);
return mountedRef;
};
Demo: https://stackblitz.com/edit/react-eelqp2
One thing important is that you have to use setTimeout to make a reasonable delay to make sure that the ref value is set to true after all initial useEffects.
You can use custom hook to run use effect after mount.
const useEffectAfterMount = (cb, dependencies) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
useEffectAfterMount(() => {
console.log("fetching");
dispatch(fetchData(page, pageSize, search, sorts));
}, [page, pageSize, search, sorts]);
Here is the typescript version:
const useEffectAfterMount = (cb: EffectCallback, dependencies: DependencyList | undefined) => {
const mounted = useRef(true);
useEffect(() => {
if (!mounted.current) {
return cb();
}
mounted.current = false;
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
I know this is late to the game but I think it's also worth noting that for what the OP is trying to accomplish, data caching would be a more wholistic and scalable solution. Libraries like react-query are great for this.

React-hooks. Can't perform a React state update on an unmounted component

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
}
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.
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;
};
}, []);
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.
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"
}
})
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();
)
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
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
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
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
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>;
}

Resources