Cancelling tasks in the componentWillUnmount - javascript

This is a common problem, yet I know why and can generally fix quickly.
However, on this occasion, I cannot seem to unmount the tasks in my ReactJS and GatsbyJS application.
The code below is listening to Firebase auth changes and the with setState is making the auth users details available within state
_initFirebase = false;
constructor(props) {
super(props);
this.state = {
authUser: null
};
}
firebaseInit = () => {
const { firebase } = this.props;
if (firebase && !this._initFirebase) {
this._initFirebase = true;
this.listener = firebase.onAuthUserListener(
authUser => {
localStorage.setItem('authUser', JSON.stringify(authUser));
this.setState({ authUser });
},
() => {
localStorage.removeItem('authUser');
this.setState({ authUser: null });
}
);
}
};
componentDidMount() {
this.setState({
authUser: JSON.parse(localStorage.getItem('authUser'))
});
this.firebaseInit();
}
componentDidUpdate() {
this.firebaseInit();
}
componentWillUnmount() {
this.listener && this.listener();
}
Causing an error in the console of
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 the componentWillUnmount method.
in WithAuthentication (created by Context.Consumer)
in Component (created by Layout)
in Layout (created by SigninPage)
in SigninPage (created by HotExportedSigninPage)
in AppContainer (created by HotExportedSigninPage)
in HotExportedSigninPage (created by PageRenderer)
From my understanding, I have sufficiently unmount these setState tasks within componentWillUnmount.
Could you please explain what I may have missed?

The problem is you are trying to setState after componentWillUnmount triggered...
you can not setState in componentWillUnmount.
The solution for your use case :
initFirebase = false;
constructor(props) {
super(props);
this.state = {
authUser: null
};
// this prop to check component is live or not
this.isAmAlive = false;
}
firebaseInit = () => {
const { firebase } = this.props;
if (firebase && !this._initFirebase) {
this._initFirebase = true;
this.listener = firebase.onAuthUserListener(
authUser => {
localStorage.setItem('authUser', JSON.stringify(authUser));
//check component is live or not if live update the component
if(this.isAmAlive){
this.setState({ authUser });
}
},
() => {
localStorage.removeItem('authUser');
//check component is live or not if live update the component
if(this.isAmAlive){
this.setState({ authUser : null });
}
}
);
}
};
componentDidMount() {
this.isAmAlive =true;
this.setState({
authUser: JSON.parse(localStorage.getItem('authUser'))
});
this.firebaseInit();
}
componentDidUpdate() {
this.firebaseInit();
}
componentWillUnmount() {
this.isAmAlive = false;
this.listener && this.listener();
}

Related

React memory leak - when updating state in context provider via a function passed to a child of the provider

After some debugging I understand the issue and I know roughly why it's happening, so I will show as much code as I can.
The Error
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 the componentWillUnmount method.
in ProductsDisplay (created by ConnectFunction)
in ConnectFunction (created by Context.Consumer)
in Route (created by SiteRouter)
in Switch (created by SiteRouter)
in SiteRouter (created by ConnectFunction)
in ConnectFunction (created by TLORouter)
in Route (created by TLORouter)
in Switch (created by TLORouter)
So to give you context, the React structure looks a bit like so
Simplified version
App.jsx > Router > GlobalLayoutProvider > Route > Page
Within the GlobalLayoutProvider I pass six functions down via the new react context, the code looks like so. All these functions provide is the ability to modify the state of the layout component, so that if child elements have more complex requirements they can send the information up after performing fetchs etc or they could on mount set the values of the layout.
GlobalLayoutRedux.jsx
class GlobalLayoutProvider extends React.Component {
constructor(props) {
super(props);
this.state = { routeConfig: null };
this.getRouteData = this.getRouteData.bind(this);
this.setLoaderOptions = this.setLoaderOptions.bind(this);
}
componentDidMount() {
this.getRouteData();
}
componentDidUpdate(prevProps) {
const { urlParams, user, layoutSettings } = this.props;
if (
urlParams.pathname !== prevProps.urlParams.pathname
|| user.permissions !== prevProps.user.permissions
) {
this.getRouteData();
}
}
getRouteData() {
const { user, urlParams } = this.props;
const { tlo, site, pathname } = urlParams;
this.setState({
routeConfig: pageConfigs().find(
(c) => c.pageContext(tlo, site, user) === pathname,
),
});
}
setLoaderOptions(data) {
this.setState((prevState) => ({
routeConfig: {
...prevState.routeConfig,
loader: {
display: data?.display || initialState.loader.display,
message: data?.message || initialState.loader.message,
},
},
}));
}
render() {
const { routeConfig } = this.state;
const { children, user } = this.props;
return (
<GlobalLayoutContext.Provider
value={{
setLoaderOptions: this.setLoaderOptions,
}}
>
<PageContainer
title={routeConfig?.pageContainer?.title}
breadcrumbs={[routeConfig?.pageContainer?.title]}
>
<ActionsBar
actionsBarProperties={{ actions: routeConfig?.actionBar?.actions }}
pageTitle={routeConfig?.actionBar?.title}
/>
<SideNav items={routeConfig?.sideNav?.options} selected={routeConfig?.sideNav?.pageNavKey}>
<div id={routeConfig?.sideNav?.pageNavKey} className="Content__body page-margin">
<div id="loader-instance" className={`${routeConfig?.loader?.display ? '' : 'd-none'}`}>
<Loader message={routeConfig?.loader?.message} />
</div>
<div id="children-instance" className={`${routeConfig?.loader?.display ? 'd-none' : ''}`}>
{children}
</div>
</div>
</SideNav>
</PageContainer>
</GlobalLayoutContext.Provider>
);
}
}
export default GlobalLayoutProvider;
Inside the Page.jsx we have a componentDidMount and a componentDidUpdate. The issue seems to stem from calling the parent function and setting the state pretty much at any point prior to updating the state of the child component.
Page.jsx
export default class Page extends Component {
static contextType = GlobalLayoutContext;
constructor(props) {
super(props);
this.state = {
someState: 'stuff'
};
}
componentDidMount() {
this.setActionBarButtons();
this.fetchOrganisationsProducts();
}
async componentDidUpdate(prevProps) {
const { shouldProductsRefresh, selectedOrganisation, permissions } = this.props;
if (
selectedOrganisation?.id !== prevProps.selectedOrganisation?.id
|| shouldProductsRefresh !== prevProps.shouldProductsRefresh
) {
await this.fetchOrganisationsProducts();
}
if (
selectedOrganisation?.id !== prevProps.selectedOrganisation?.id
|| shouldProductsRefresh !== prevProps.shouldProductsRefresh
|| permissions !== prevProps.permissions
) {
this.setActionBarButtons();
}
}
setActionBarButtons() {
const { setActionBarOptions } = this.context;
const actions = [
ActionButtons.Custom(
() => this.setState({ exportTemplateModalIsOpen: true }),
{ title: 'Button', icon: 'button' },
),
];
setActionBarOptions({ actions, title: 'Products', display: true });
}
async fetchOrganisationsProducts() {
const { selectedOrganisation } = this.props;
const { setLoaderOptions } = this.context;
setLoaderOptions({ display: true, message: 'Loading Products In Organisation' });
(await productStoreService.getProducts(selectedOrganisation.id))
.handleError(() => setLoaderOptions({ display: false }))
.handleOk((products) => {
this.setState({ products }, () => {
setLoaderOptions({ display: false });
products.forEach(this.fetchAdditionalInformation)
});
});
}
render() {
return (<p>Something</p>)
}
}
What's odd the memory leak will disappear if I add this suggestion I seen on stack overflow suggesting to track the state of the components interacting with the higher-level component.
export default class Page extends Component {
static contextType = GlobalLayoutContext;
constructor(props) {
super(props);
this.state = {
someState: 'stuff'
};
}
// ADDITION HERE
_isMounted = false;
componentDidMount() {
// ADDITION HERE
this._isMounted = true;
this.setActionBarButtons();
this.fetchOrganisationsProducts();
}
// ADDITION HERE
componentWillUnmount() {
this._isMounted = false;
}
async fetchOrganisationsProducts() {
const { selectedOrganisation } = this.props;
const { setLoaderOptions } = this.context;
setLoaderOptions({ display: true, message: 'Loading Products In Organisation' });
(await productStoreService.getProducts(selectedOrganisation.id))
.handleError(() => setLoaderOptions({ display: false }))
.handleOk((products) => {
// ADDITION HERE
if (this._isMounted) {
this.setState({ products }, () => {
setLoaderOptions({ display: false });
products.forEach(this.fetchAdditionalInformation)
});
}
});
}
render() {
return (<p>Something</p>)
}
}
Personally, I don't see this as a solution if I was building my own thing I wouldn't be too fussed but I can't ask an entire company to start adding this addition everywhere.
My gut is telling me that because the component is firing up an object to configure the state of the parent which is for a fraction of a second unmounting as the component did mount is still processing due to the async network fetch when that is returned it is saving to the state before the parent has managed to render the function call state change.
What was odd if I pass the callbacks into the parent and call them once the setState has been actioned the issue is resolved like so
setOnMountOptions(data) {
this.setState((prevState) => ({
routeConfig: {
...prevState.routeConfig,
...data?.loader ? { loader: data.loader } : {},
},
}), async () => { await data.callbacks(); });
}
but again this causes havoc on the testing side as you are abstracting the componentDidmount functionality out and calling it after a set state is actioned elsewhere.
I have tried adapting what I have to Redux but I had the exact same result everything from a viewing perspective in the browser was fine but still getting the same memory leak using the Redux calls to try to populate all the data from top to bottom.
I can't think of any way of handling this gracefully where we don't need to ask the company to add that fix everywhere.
So to save people time and effort it turns out our memory leak was actually being cause by a bad set state in the routers of our application.

Can't get rid of: Warning: Can't perform a React state update on an unmounted component

Full warning 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.
This warning is NOT showing constant, it shows whenever it feels like. MOST TIMES when the app just started.
export default class something extends React.Component {
_isMounted = false;
state = {
};
componentDidMount() {
this._isMounted = true;
firebase = new Fire((error, user) => {
if (error) {
return alert('something something something something');
}
firebase.getLists((lists) => {
this.setState({ lists, user }, () => {
this.setState({ loading: false });
});
});
this.setState({ user });
});
}
componentWillUnmount() {
this._isMounted = true;
firebase.detach();
}
this is in an another file that contains all the firebase code
detach() {
this.unsubscribe();
}
My guess it has to do with detach firebase.detach
There are a few minor updates that are needed in your code for it to work correctly.
You'll want to check if _isMounted is true before updating any state variables.
You'll also want to set _isMounted=false in componentWillUnmount() instead of _isMounted=true.
See the updated code below:
export default class something extends React.Component {
_isMounted = false;
state = {
};
componentDidMount() {
this._isMounted = true;
firebase = new Fire((error, user) => {
if (error) {
return alert('something something something something');
}
firebase.getLists((lists) => {
if (this._isMounted){
this.setState({ lists, user }, () => {
this.setState({ loading: false });
});
}
});
if (this._isMounted){
this.setState({ user });
}
});
}
componentWillUnmount() {
this._isMounted = false;
firebase.detach();
}

setState error on unmounted component when with data from Firebase

When the component below is mounted, everything Firebase related works fine. The issue occurs when the data in Firebase is updated. I then navigate to a different route, therefore un-mounting this component and the setState error occurs.
Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component
I have tried turning the Firebase functions 'off' in componentWillUnmount by i still seem to be hit with the error. Any help would be appreciated
constructor() {
super();
this.state = {
firebaseData: {}
};
}
componentDidMount() {
const { referenceId } = this.props.episode || '';
if (referenceId) {
this.getFirebaseData(this.removeDissallowedChars(referenceId));
}
}
componentWillReceiveProps(nextProps) {
if (this.props.values.referenceId !== nextProps.values.referenceId) {
this.setState({
referenceId: nextProps.values.referenceId,
}, this.fetchWorkflows);
}
}
getFirebaseData(refId) {
const database = firebase.database().ref(`workflows/sky/${refId}`);
database.on('value', snapshot => {
this.setState({ firebaseData: snapshot.val() });
}, error =>
console.log(error)
);
}
componentWillUnmount(refId) {
const database = firebase.database().ref(`workflows/sky/${refId}`);
database.off();
}
removeDissallowedChars(badRefId) {
/**
* BE strip characters that Firebase doesn't allow.
* We need to do the same. Reference id will only contain the characters listed below.
* Change needed in FE as some of our reference id's currently contain period characters.
**/
return badRefId.replace(/[^A-Za-z0-9-:/]+/g, '-');
}
fetchWorkflows() {
const { referenceId } = this.state;
this.props.fetchWorkflows(referenceId);
}
You can have a class variable that keeps track of whether or not your component is mounted. That would look like this:
constructor() {
//...
this._mounted = false;
}
componentDidMount() {
this._mounted = true;
//...
}
componentWillUnmount() {
//...
this._mounted = false;
}
Then on any place you set the state after an async request, you can put an if statement that checks whether or not _mounted is true.
In your case:
getFirebaseData(refId) {
const database = firebase.database().ref(`workflows/sky/${refId}`);
database.on('value', snapshot => {
// Check if component is still mounted.
if (this._mounted) {
this.setState({ firebaseData: snapshot.val() });
}
}, error =>
console.log(error)
);
}

React Warning: Can't call setState (or forceUpdate) on an unmounted component

I have 2 components:
Orders - fetch some data and display it.
ErrorHandler - In case some error happen on the server, a modal will show and display a message.
The ErrorHandler component is warping the order component
I'm using the axios package to load the data in the Orders component, and I use axios interceptors to setState about the error, and eject once the component unmounted.
When I navigate to the orders components back and forward i sometimes get an error in the console:
Warning: Can't call setState (or forceUpdate) 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 the componentWillUnmount method.
in Orders (at ErrorHandler.jsx:40)
in Auxiliary (at ErrorHandler.jsx:34)
in _class2 (created by Route)
I tried to solve it by my previous case React Warning: Can only update a mounted or mounting component but here I can't make an axios token by the inspectors. Has anyone solved this issue before?
Here are my components:
Orders:
import React, { Component } from 'react';
import api from '../../api/api';
import Order from '../../components/Order/Order/Order';
import ErrorHandler from '../../hoc/ErrorHandler/ErrorHandler';
class Orders extends Component {
state = {
orders: [],
loading: true
}
componentDidMount() {
api.get('/orders.json')
.then(response => {
const fetchedOrders = [];
if (response && response.data) {
for (let key in response.data) {
fetchedOrders.push({
id: key,
...response.data[key]
});
}
}
this.setState({ loading: false, orders: fetchedOrders });
})
.catch(error => {
this.setState({ loading: false });
});
}
render() {
return (
<div>
{this.state.orders.map(order => {
return (<Order
key={order.id}
ingrediencies={order.ingrediencies}
price={order.price} />);
})}
</div>
);
}
}
export default ErrorHandler(Orders, api);
ErrorHandler:
import React, { Component } from 'react';
import Auxiliary from '../Auxiliary/Auxiliary';
import Modal from '../../components/UI/Modal/Modal';
const ErrorHandler = (WrappedComponent, api) => {
return class extends Component {
requestInterceptors = null;
responseInterceptors = null;
state = {
error: null
};
componentWillMount() {
this.requestInterceptors = api.interceptors.request.use(request => {
this.setState({ error: null });
return request;
});
this.responseInterceptors = api.interceptors.response.use(response => response, error => {
this.setState({ error: error });
});
}
componentWillUnmount() {
api.interceptors.request.eject(this.requestInterceptors);
api.interceptors.response.eject(this.responseInterceptors);
}
errorConfirmedHandler = () => {
this.setState({ error: null });
}
render() {
return (
<Auxiliary>
<Modal
show={this.state.error}
modalClosed={this.errorConfirmedHandler}>
{this.state.error ? this.state.error.message : null}
</Modal>
<WrappedComponent {...this.props} />
</Auxiliary>
);
}
};
};
export default ErrorHandler;
I think that's due to asynchronous call which triggers the setState, it can happen even when the component isn't mounted. To prevent this from happening you can use some kind of flags :
state = {
isMounted: false
}
componentDidMount() {
this.setState({isMounted: true})
}
componentWillUnmount(){
this.state.isMounted = false
}
And later wrap your setState calls with if:
if (this.state.isMounted) {
this.setState({ loading: false, orders: fetchedOrders });
}
Edit - adding functional component example:
function Component() {
const [isMounted, setIsMounted] = React.useState(false);
useEffect(() => {
setIsMounted(true);
return () => {
setIsMounted(false);
}
}, []);
return <div></div>;
}
export default Component;
You can't set state in componentWillMount method. Try to reconsider your application logic and move it into another lifecycle method.
I think rootcause is the same as what I answered yesterday, you need to "cancel" the request on unmount, I do not see if you are doing it for the api.get() call in Orders component.
A note on the Error Handling, It looks overly complicated, I would definitely encourage looking at ErrorBoundaries provided by React. There is no need for you to have interceptors or a higher order component.
For ErrorBoundaries, React introduced a lifecycle method called: componentDidCatch.
You can use it to simplify your ErrorHandler code to:
class ErrorHandler extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error, info) {
this.setState({ hasError: true, errorMessage : error.message });
}
render() {
if (this.state.hasError) {
return <Modal
modalClosed={() => console.log('What do you want user to do? Retry or go back? Use appropriate method logic as per your need.')}>
{this.state.errorMessage ? this.state.errorMessage : null}
</Modal>
}
return this.props.children;
}
}
Then in your Orders Component:
class Orders extends Component {
let cancel;
state = {
orders: [],
loading: true
}
componentDidMount() {
this.asyncRequest = api.get('/orders.json', {
cancelToken: new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
cancel = c;
})
})
.then(response => {
const fetchedOrders = [];
if (response && response.data) {
for (let key in response.data) {
fetchedOrders.push({
id: key,
...response.data[key]
});
}
}
this.setState({ loading: false, orders: fetchedOrders });
})
.catch(error => {
this.setState({ loading: false });
// please check the syntax, I don't remember if it is throw or throw new
throw error;
});
}
componentWillUnmount() {
if (this.asyncRequest) {
cancel();
}
}
render() {
return (
<div>
{this.state.orders.map(order => {
return (<Order
key={order.id}
ingrediencies={order.ingrediencies}
price={order.price} />);
})}
</div>
);
}
}
And use it in your code as:
<ErrorHandler>
<Orders />
</ErrorHandler>

Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application

Why am I getting this error?
Warning: Can't call setState (or forceUpdate) 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 the componentWillUnmount method.
postAction.js
export const getPosts = () => db.ref('posts').once('value');
components:
constructor(props) {
super(props);
this.state = { posts: null };
}
componentDidMount() {
getPosts()
.then(snapshot => {
const result = snapshot.val();
this.setState(() => ({ posts: result }));
})
.catch(error => {
console.error(error);
});
}
componentWillUnmount() {
this.setState({ posts: null });
}
render() {
return (
<div>
<PostList posts={this.state.posts} />
</div>
);
}
As others mentioned, the setState in componentWillUnmount is unnecessary, but it should not be causing the error you're seeing. Instead, the likely culprit for that is this code:
componentDidMount() {
getPosts()
.then(snapshot => {
const result = snapshot.val();
this.setState(() => ({ posts: result }));
})
.catch(error => {
console.error(error);
});
}
since getPosts() is asynchronous, it's possible that before it can resolve, the component has unmounted. You're not checking for this, and so the .then can end up running after the component has unmounted.
To handle that, you can set a flag in willUnmount, and check for that flag in the .then:
componentDidMount() {
getPosts()
.then(snapshot => {
if (this.isUnmounted) {
return;
}
const result = snapshot.val();
this.setState(() => ({ posts: result }));
})
.catch(error => {
console.error(error);
});
}
componentWillUnmount() {
this.isUnmounted = true;
}
React component's state is a local entity. Unmounted component dont have state, no need to do that. React is already telling you this is a no-op which means no-operation in tech speak. It means that you telling component to do something when its already destroyed.
https://reactjs.org/docs/react-component.html#componentwillunmount
You should not call setState() in componentWillUnmount() because the component will never be re-rendered. Once a component instance is unmounted, it will never be mounted again.
Remove this
componentWillUnmount() {
this.setState({ posts: null });
}
it's useless
From the doc:
You should not call setState() in componentWillUnmount() because the
component will never be re-rendered. Once a component instance is
unmounted, it will never be mounted again.
https://reactjs.org/docs/react-component.html#componentwillunmount
You can try this code:
constructor(props) {
super(props);
this.state = { posts: null };
}
_isMounted = false;
componentDidMount() {
this._isMounted = true;
getPosts()
.then(snapshot => {
const result = snapshot.val();
if(this._isMounted) {
this.setState(() => ({ posts: result }))
}
})
.catch(error => {
console.error(error);
});
}
componentWillUnmount() {
this._isMounted = false;
this.setState({ posts: null });
}
render() {
return (
<div>
<PostList posts={this.state.posts} />
</div>
);
}
By using _isMounted, setState is called only if the component is mounted. the answer simply does a check to see if the component is mounted before setting the state.

Categories

Resources