Description
I have component which shows data that get from server and display it on the table using the state, tableData and it must be set when Redux action is dispatched.
I've use action listener library which uses Redux middleware which consisting of 63 lines of code. redux-listeners-qkreltms.
For example when I register a function on analysisListIsReady({}).type which is ANALYSISLIST_IS_READY then when the action is dispatched, the function is called.
Issue
The issue is that react throws sometimes the error: Can't update react state... for setTableData so response data is ignored to be set. I want to figure it out when it happens.
I've assumed that it's because of unmounting of component, so I printed some logs, but none of logs are printed and also ComponentA is not disappeared.
It's not throing any error when I delete getAnalysisJsonPathApi and getResource, so I tried to reporuduce it, but failed... link
It's not throing any error when I delete listenMiddleware.addListener see: #2
#1
// ComponentA
const [tableData, setTableData] = useState([])
useEffect(() => {
return () => {
console.log("unmounted1")
}}, [])
useEffect(() => {
listenMiddleware.addListener(analysisListIsReady({}).type, (_) => {
try {
getAnalysisJsonPathApi().then((res) => {
//...
getResource(volumeUrl)
.then((data: any) => {
// ...
setTableData(data)
})
})
} catch (error) {
warn(error.message)
}
})
return () => {
console.log("unmounted2")
}
}, [])
export const getAnalysisJsonPathApi = () => {
return api
.post('/segment/volume')
.then(({ data }) => data)
export const getResource = async (src: string, isImage?: boolean): Promise<ArrayBuffer> =>
api
.get(src)
.then(({ data }) => data)
#2
// ComponentA
const [tableData, setTableData] = useState([])
useEffect(() => {
return () => {
console.log("unmounted1")
}}, [])
useEffect(() => {
if (steps.step2a) {
try {
getAnalysisJsonPathApi().then((res) => {
//...
getResource(volumeUrl)
.then((data: any) => {
// ...
setTableData(data)
})
})
} catch (error) {
warn(error.message)
}
}
return () => {
console.log("unmounted2")
}
}, [steps.step2a])
Well, its as you said:
because of unmounting of component
In your UseEffect() function, you need to check if the componenet is mounted or not, in other words, you need to do the componentDidMount & componentDidUpdate (if needed) logics:
const mounted = useRef();
useEffect(() => {
if (!mounted.current) {
// do componentDidMount logic
console.log('componentDidMount');
mounted.current = true;
} else {
// do componentDidUpdate logic
console.log('componentDidUpdate');
}
});
i didn't go to your question code detail, but my hint might help you, usually this error happens in fetchData function,
suppose you have a fetchData function like below:
fetchData(){
...
let call = await service.getData();
...
--->setState(newItems)//Here
}
so when api call end and state want to be updated, if component been unmounted, there is no state to be set,
you can use a bool variable and set it false when component will unmount:
let stillActive= true;
fetchData(){
active = true;
...
let call = await service.getData();
...
if(stillActive)
setState(newItems)//Here
}
}
componentWillUnmount(){
active = false;
}
I've found out it's because of redux-listeners-qkreltms, Redux middleware.
It keeps function when component is mounted into listener, but never changes its functions even component is unmounted.
middleware.addListener = (type, listener) => {
for (let i = 0; i < listeners.length; i += 1) {
if (listeners[i].type === type) {
return;
}
}
listeners.push(createListener(type, listener));
};
Related
I need to make a button click handler which have a few other function calls in it. One of them is a onAccept function which has a few setStates in it and want to wait until them all is done. Is there a way to make onAccept synchronous?
button click handler
const onUpdateBoundaries = async (recommendation) => {
await getSnippetIndex(
//some props
).then(response => {
onAccept({...recommendation, index: response});
});
fetchRecommendations() //<- this function shouldn't be called until onAccept's setStates are done
};
onAccept
const onAccept = (recommendation) => {
setAccepted((accepted) => [
...new Set([...accepted, ...recommendation.cluster_indices.map(recommendation => recommendation.index)]),
]);
setRejected((rejected) => [
...new Set(removeFromArray(rejected, recommendation.cluster_indices.map(recommendation => recommendation.index)))
]);
};
fetchRecommendations
const fetchRecommendations = async () => {
try {
const {//some props
propagated_accepted,
propagated_rejected,
} = await getRecommendations(
//some props
);
setAccepted((accepted) => [...accepted, ...propagated_accepted]);
setRejected((rejected) => [...rejected, ...propagated_rejected]);
} catch (err) {
//handling
}
setIsWaitingForRecommendations(false);
};
You can try with useEffect and useRef to achieve it
//track all previous values before state updates
const previousValues = useRef({ rejected, accepted });
useEffect(() => {
//only call `fetchRecommendations` once both `rejected` and `accepted` get updated
if(previousValues.current.rejected !== rejected && previousValues.current.accepted !== accepted) {
fetchRecommendations()
}
}, [rejected, accepted])
Another easier way that you can try setState, which is the old-school function with callback (the problem with this solution is you need to use class component - NOT function component)
const onAccept = (recommendation) => {
setState((prevState) => ({
accepted: [
...new Set([...prevState.accepted, ...recommendation.cluster_indices.map(recommendation => recommendation.index)]),
],
rejected: [
...new Set(removeFromArray(prevState.rejected, recommendation.cluster_indices.map(recommendation => recommendation.index)))
]
}), () => {
//callback here
fetchRecommendations()
})
}
React is declarative, which means it will control the setState function calls incl. batching them if necessary to optimise performance.
What you can do is make use of a useEffect to listen for changes in state and run code you need to run after state change there.
For eg: ( I'm assuming your two states are accepted and rejected)
useEffect(() => {
fetchRecommendations() //<- gets called everytime accepted or rejected changes
}, [accepted, rejected])
// onAccept remains the same
//button click handler
const onUpdateBoundaries = async (recommendation) => {
const response = await getSnippetIndex( //some props )
onAccept({...recommendation, index: response});
};
If you want to run it only if current values of accepted or rejected has changed, you can make use of use Ref to store the previous values of accepted and rejected.
You can create a custom hook like
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
Then
// import usePrevious hook
const prevAccepted = usePrevious(accepted)
const prevRejected = usePrevious(rejected)
useEffect(() => {
if(prevAccepted!=accepted && prevRejected!=rejected)
fetchRecommendations() //<- gets called everytime accepted or rejected changes
}, [accepted, rejected])
const onUpdateBoundaries = async (recommendation) => {
const response = await getSnippetIndex( //some props )
onAccept({...recommendation, index: response});
};
Think something like this would do the trick. Let me know if this works :)
you can make a async method like this
const SampleOfPromise = () => {
onClick=async()=>{
await myPromise();
}
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('sample');
}, 300);
});
return(
<Button onClick={onClick}>
</Button>
)
}
I want to know how to run the useEffect side effect in both component mounting and a dependent value change. Currently I'm using two useEffects to achieve this like this.
useEffect(() => {
let isMounted = true;
const getUsers = async () => {
try {
const userResponse = await api.get('/users');
if (isMounted) { setUsers(userResponse.data); }
} catch (error) {
console.log(error);
}
};
getUsers();
}, []);
useEffect(() => {
let isMounted = true;
const getUsers = async () => {
try {
const userResponse = await api.get('/users');
if (isMounted) { setUsers(userResponse.data); }
} catch (error) {
console.log(error);
}
};
getUsers();
}, [netInfo]);
Is there anyway to achieve this using one useEffect?
Runs when the component is mounted for the first time and on every re-render
useEffect(() => {})
Runs when the component is mounted for the first time alone
useEffect(() => {}, [])
Runs when the component is mounted for the first time and whenever the someDependency's value changes .
useEffect(() => {}, [someDependency])
You can remove the first useEffect .
I need help extracting a database listener function into its own callable function so I can unsubscribe from it. Here is what I have:
const reroute = (user) => {
// A function to route user based on its state values
};
export function AuthenticateUser() {
const user = useStore((state) => state.user);
const setUser = useStore((state) => state.setUser);
useEffect(() => {
let unsubscribe;
function setupUser(userDetails) {
Analytics.setUserId(userDetails!.id);
setUser(userDetails);
}
if (!user) {
// LISTENER 1 : Authenticate user. We unsubscribe from this listener in the cleanup function.
unsubscribe = firebase.auth().onAuthStateChanged((authenticatedUser) => {
if (authenticatedUser) {
const uid = authenticatedUser.uid;
const db = firebase.firestore();
// LISTENER 2: This is the listener I need to be able to unsubscribe from wherever I need.
const unsubscribeUserUpdates = db.collection("users").doc(uid)
.onSnapshot(
async (doc) => {
// onSnapshot listener will fire this whenever something updates on the server
const documentData = doc.data();
const userData = {
id: doc.id,
...documentData,
};
setupUser(userData);
setLoadingUser(false);
reroute(userData);
},
(error) => {
console.log("error", error);
alertMessage("danger", "User authentication failed", error.message);
reportError("GQL_getOrCreateUser", error);
}
);
} else {
// No authenticated user
setLoadingUser(false);
reroute(null);
}
});
}
return () => {
unsubscribe?.();
// unsubscribeUserUpdates?.(); << If I run this, then the listener will be terminated, but I don't want to terminate it here.
};
}, [GQL_getOrCreateUser, setLoadingUser, setUser, user]);
}
I load AuthenticateUser into my react navigation index.ts
export default function Navigation() {
AuthenticateUser();
return (
<NavigationContainer>
...
I have a Sign Out function here which fires on button click that should sign the user out and terminate the unsubscribeUserUpdates listener
export default function AccountNavigator() {
const setUser = useStore((state) => state.setUser);
function signOut() {
setUser(null);
firebase.auth().signOut();
}
return (
<AccountStack.Navigator>
...
I want to be able to terminate the unsubscribeUserUpdates listener, but I think that for that I need to have it as a callable function. I tried extracting it to a useCallback hook, but got errors.
Could someone please let me know, with code example, what to do to be able to load the listener in AuthenticateUser and stop it anywhere I need?
You can make a new useEffect for unmounting purpose. Empty dependencies make the useEffect work as componentDidMount and returning that makes it componentWillUnmount
useEffect(() => {
const unsubscribe = someAuthStuff();
return () => {
unsubscribe();
};
}, []);
You should initialize a variable
on useEffect try this:
mounted = true
...
mounted && fun();
return () => (mounted = false);
I have a React component using hooks like this:
const myComponent = (props) => {
useEffect(() => {
FetchData()
.then(data => {
setState({data: data});
}
// some other code
}, []);
//some other code and render method...
}
fetchData is in charge to use axios and get the data from an API:
const FetchData = async () => {
try {
res = await myApiClient.get('/myEndpoint);
} catch (err) {
console.log('error in FetchData');
res = err.response
}
}
and finally myApiClient is defined externally. I had to use this setup in order to be able to use different APIs...
import axios from "axios";
axios.defaults.headers.post["Content-Type"] = "application/json";
const myApiClient = axios.create({
baseURL: process.env.REACT_APP_API1_BASEURL
});
const anotherApiClient = axios.create({
baseURL: process.env.REACT_APP_API2_BASEURL
});
export {
myApiClient,
anotherApiClient
};
with this setup I am getting the 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 googled a bit and I saw some suggestions on how to clean up requests from useEffect, like this, but my axios is defined externally. So how can I send the cancellation using this setup?
Also, the application is using redux, not sure if it is in some way involved.
Any other suggestion to avoid the error is welcome.
You can use defer from rxjs for this:
const FetchData = () => {
try {
return myApiClient.get("/myEndpoint");
} catch (err) {
console.log("error in FetchData");
return err.response;
}
};
const myComponent = (props) => {
useEffect(() => {
const subscription = defer(FetchData()).subscribe({
next: ({
data
}) => {
setState({
data: data
});
},
error: () => {
// error handling
},
complete: () => {
// cancel loading state etc
}
});
return () => subscription.unsubscribe();
}, []);
}
Alway check if you are dealing with fetch or any long operations.
let _isMounted = false;
const HooksFunction = props => {
const [data, setData] = useState({}); // data supposed to be object
const fetchData = async ()=> {
const res = await myApiClient.get('/myEndpoint');
if(_isMounted) setData(res.data); // res.data supposed to return an object
}
useEffect(()=> {
_isMounted = true;
return ()=> {
_isMounted = false;
}
},[]);
return (
<div>
{/*....*/}
<div/>
);
}
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>;
}