This question already has answers here:
React Hook Warnings for async function in useEffect: useEffect function must return a cleanup function or nothing
(17 answers)
Closed 4 days ago.
I have the code below
const [status, statusSetter] = useState({ isAuthenticated: false });
useEffect(() => {
let didCancel = false;
async function fetchMyAPI() {
if (!didCancel) {
let response = (await axios.get('api/auth/getuserstatus')).data;
statusSetter(response);
}
}
fetchMyAPI();
return () => {
didCancel = true;
}
}, []);
I have tried implementing the didcancel as a form of clean up for userEffects but it doesnt work. I get the following 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. "
what am i supposed to put in my clean up?
That variable should only prevent calling statusSetter:
async function fetchMyAPI() {
let response = (await axios.get('api/auth/getuserstatus')).data;
!didCancel && statusSetter(response);
}
But, basically, this is a hack, since this approach doesn't clean up anything. We keep useless background work and just don't change the component state in the result. We should abort the pending get request in some way.
With a custom experimental hook we can write auto-cancellable async routines. For example the following json request will be aborted automatically if the component unmounted while fetching (Live demo to play) :
import React, { useState } from "react";
import { useAsyncEffect, E_REASON_UNMOUNTED } from "use-async-effect2";
import cpAxios from "cp-axios";
export default function TestComponent(props) {
const [text, setText] = useState("");
const cancel = useAsyncEffect(function* () {
const response = yield cpAxios(props.url);
setText(`Success: ${JSON.stringify(response.data)}`);
},
[props.url]
);
return (...)
}
This code should work for you:
const [status, statusSetter] = useState({ isAuthenticated: false });
let didCancel = false;
useEffect(() => {
async function fetchMyAPI() {
let response = (await axios.get('api/auth/getuserstatus')).data;
if (!didCancel) {
statusSetter(response);
}
}
fetchMyAPI();
return () => {
didCancel = true;
}
}, []);
With axios, you can do it this way:
useEffect(() => {
const source = axios.CancelToken.source();
fetchMyAPI(source);
return () => {
source.cancel();
};
}, []);
Then in the fetch function
const fetchMyAPI = async (source) => {
try {
const response = await axios({
cancelToken: source.token,
});
} catch (err) {
if (!axios.isCancel(err)) {
// TO DO...
}
}
};
Related
Hi I created a React hook to handle Axios requests. The trouble I'm having is that I'm unable to successfully abort the request to prevent memory leak. When I switch between 2 pages that uses the hook, I get 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.
useAxios hook code:
import { useState, useEffect } from "react";
import axios from "axios";
const useAxios = (url, method = "GET") => {
const [data, setData] = useState([]);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [body, setBody] = useState(null);
useEffect(() => {
const controller = new AbortController();
const getData = async (body) => {
setLoading(true);
try {
const res = body
? await axios.post(url, body, {
signal: controller.signal,
})
: await axios.get(url, {
signal: controller.signal,
});
setData(res.data);
setError(null);
setLoading(false);
} catch (err) {
console.log(err.message);
setError(err.message);
setLoading(false);
}
};
if (method === "GET") {
getData();
} else if (method === "POST") {
getData(body);
}
return () => {
controller.abort();
};
}, [url, method, body]);
return { data, error, loading, setBody };
};
export default useAxios;
Your setError and setLoading are both going to fire when aborting.
So you will want to set a flag in the unmount callback.
You can then conditionally call setError / setLoading based on this.
useEffect(() => {
let aborting = false;
try {
....
} catch (err) {
console.log(err.message);
if (!aborting) {
setError(err.message);
setLoading(false);
}
}
....
return () => {
aborting = true;
controller.abort();
}
}, ...
prevent memory leak
Just a note, this wound't actually be a memory leak, it's just a warning that it could have come from one. eg. using addEventListener without removeEventListener. But in either case you would still want to prevent a setState on a umounted component.
I am relatively new to react hooks and I am trying to create this custom hook to handle CRUD operations for my API.
This is the hook file:
import React, { useState, useEffect } from "react";
const useApi = (url, headers = { method: "GET" }, payload = null) => {
const [isLoading, setIsLoading] = useState(true);
const [apiData, setApiData] = useState(null);
const [serverError, setServerError] = useState(null);
const [api, setApi] = useState({});
const list = async () => {
try {
const resp = await fetch(url);
const data = await resp?.json();
setApiData(data);
setIsLoading(false);
} catch (error) {
setServerError(error);
} finally {
setIsLoading(false);
}
};
const create = async () => {
try {
const resp = await fetch(url, (headers = { method: "POST" }), payload);
const data = await resp?.json();
setApiData(data);
setIsLoading(false);
} catch (error) {
setServerError(error);
} finally {
setIsLoading(false);
}
};
setApi({
...api,
list: list,
create: create
});
return { isLoading, apiData, serverError, api };
};
export default useApi;
However, when I call api.list() in my main component inside a useEffect() hook, I get an infinite loop.
Sample component call:
import { useEffect } from "react";
import useApi from "./useApi";
export default function App() {
const {
isLoading: loading,
apiData: students,
serverError: error,
api
} = useApi("https://59f0f160ce72350012bec011.mockapi.io/students");
console.log(loading, students, error, api);
useEffect(() => {
api.list();
}, [api]);
return (
<div className="App">
<h1>list</h1>
{loading ? "loading" : students.map((x) => x.name)}
</div>
);
}
Here's the sandbox for it:
https://codesandbox.io/s/cocky-chebyshev-d9q89?file=/src/App.js:0-492
Can anyone help me understand the issue?
Thank you in advance!
This is what is causing the infinite loop:
setApi({
...api,
list: list,
create: create
});
You are not supposed to call setState() during a render.
In your case, you don't need to useState for the api object, you can just return it on every render:
return {
isLoading,
apiData,
serverError,
api: { list, create }
};
Here is a link to the fixed sandbox
Also, another warning: this code will repeatedly call api.list().
useEffect(() => {
api.list();
}, [api]);
Since api changes on every render, it will repeatedly call api.list().
This is the object that changes on every render:
return { isLoading, apiData, serverError, api };
You can ensure that you only call api.list() one time by using a ref.
import { useRef } from 'react'
// In the component
const gotRef = useRef(false)
useEffect(() => {
if (!gotRef.current) {
api.list();
gotRef.current = true
}
}, [api]);
I have a lot of functions looking like this
doSomething = async (...) => {
try {
this.setState({loading: true});
...
var result = await Backend.post(...);
...
this.setState({loading: false});
} catch(err) {
this.setState({error: err});
}
}
Basically I have 2 variables loading & error that I have to manage for a lot of functions and the code is basically the same for all of them. Since there are no decorators in javascript and I do not wish to install any experimental lib for that how could I wrap this function to remove the duplicated setStates from above ?
Here is my current way, I pass the function as parameter.
We have many API, fetch data form backend, we have to handle error and do something with data.
Only data of service are different, the handling error is the same.
private processServiceResponse(resp: any, doSthWithData: (data: any) => void) {
let { errors } = resp;
if (this.hasError(errors)) {
this.handleServiceErr(errors);
return;
}
let data = resp;
if (resp && resp.data) {
data = resp.data;
}
doSthWithData(data);
}
And here is how i pass function as parameter.
let rest1 = service1.getData();
processServiceResponse(rest1,(data)=>{
//only need to focus with processing data.
})
PS: It's typescript coding.
if you are using function conponents, you can define a custom hook to avoid repeat code
//useFetch.js
import { useState, useEffect } from 'react';
import axios from 'axios';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
setLoading('loading...')
setData(null);
setError(null);
const source = axios.CancelToken.source();
axios.get(url, { cancelToken: source.token })
.then(res => {
setLoading(false);
//checking for multiple responses for more flexibility
//with the url we send in.
res.data.content && setData(res.data.content);
res.content && setData(res.content);
})
.catch(err => {
setLoading(false)
setError('An error occurred. Awkward..')
})
return () => {
source.cancel();
}
}, [url])
return { data, loading, error }
export default useFetch;
usage:
import useFetch from './useFetch';
import './App.css';
function App() {
const { data: quote, loading, error } =
useFetch('https://api.quotable.io/random')
return (
<div className="App">
{ loading && <p>{loading}</p> }
{ quote && <p>"{quote}"</p> }
{ error && <p>{error}</p> }
</div>
);
}
export default App;
You can use a Higher Order Function (a function that takes as argument another function) to make the common loading and error functionality reusable. It is very similar to a decorator pattern. For example:
const doSomething = withLoadingAndErrorHandling(Backend.post, this.setState);
function withLoadingAndErrorHandling(fn, setState) {
return async function(...args) {
try {
setState({loading: true});
var result = await fn(args);
setState({loading: false});
return result;
} catch(err) {
setState({error: err});
}
}
}
Is there any reason a function in a react component would run multiple times although it is being invoked only once?
I have this test function that auto-invokes itself as in:
let xfunction = (() =>
{
console.log('test');
}
)();
This is what I get in the console:
App.js Source Code:
import React, { useState, useEffect } from 'react';
import { getAllPokemons } from './services/pokemon'
import './App.css';
function App() {
const [pokemonData, setPokemonData] = useState([]);
const [loading, setLoading] = useState(true);
const initialUrl = 'payload.json'
useEffect(() => {
async function fetchData() {
let response = await getAllPokemons(initialUrl);
console.log(response);
await loadingPokemon(response.pokemon);
setLoading(false);
}
fetchData();
}, [])
const loadingPokemon = async (data) => {
let _pokemon = await Promise.all(data.map(async pokemon => {
return pokemon;
})
);
setPokemonData(_pokemon);
}
// Start - Filter by Pokemon Type
let pokemonTypes = (() =>
{
console.log('test');
}
)();
// End - Filter by Pokemon Type
return (
<div></div>
);
}
export default App;
The function is not being called or references anywhere else!
Thanks!
Solution as proposed by #Jared Smith:
If you do not want your functions rerendered and you are using react hooks, please include them in the useEffect hook as in:
useEffect(() => {
async function fetchData() {
let response = await getAllPokemons(initialUrl);
await loadingPokemon(response.pokemon);
setLoading(false);
}
fetchData();
includeFunctionHere();
}, [])
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>;
}