I'm new using React, so this might be really simple to achieve but I can't figure it out by myself even though I've done some research. Forgive me if this is too dumb.
I'm using Inertia.js with the Laravel (backend) and React (front-end) adapters. If you don't know Inertia, it basically:
Inertia.js lets you quickly build modern single-page React, Vue and Svelte apps using classic server-side routing and controllers.
I'm doing a simple login page that has a form that when submitted will perform a POST request to load the next page. It seems to work fine but in other pages the console shows the following warning:
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.
in login (created by Inertia)
The related code (I've simplified it to avoid irrelevant lines):
import React, { useEffect, useState } from 'react'
import Layout from "../../Layouts/Auth";
{/** other imports */}
const login = (props) => {
const { errors } = usePage();
const [values, setValues] = useState({email: '', password: '',});
const [loading, setLoading] = useState(false);
function handleSubmit(e) {
e.preventDefault();
setLoading(true);
Inertia.post(window.route('login.attempt'), values)
.then(() => {
setLoading(false); // Warning : memory leaks during the state update on the unmounted component <--------
})
}
return (
<Layout title="Access to the system">
<div>
<form action={handleSubmit}>
{/*the login form*/}
<button type="submit">Access</button>
</form>
</div>
</Layout>
);
};
export default login;
Now, I know that I have to do a cleanup function because the promise of the request is what is generating this warning. I know that I should use useEffect
but I don't know how to apply it in this case. I've seen example when a value change, but how to do it in a call of this kind?
Thanks in advance.
As requested, the full code of this component:
import React, { useState } from 'react'
import Layout from "../../Layouts/Auth";
import { usePage } from '@inertiajs/inertia-react'
import { Inertia } from "@inertiajs/inertia";
import LoadingButton from "../../Shared/LoadingButton";
const login = (props) => {
const { errors } = usePage();
const [values, setValues] = useState({email: '', password: '',});
const [loading, setLoading] = useState(false);
function handleChange(e) {
const key = e.target.id;
const value = e.target.value;
setValues(values => ({
...values,
[key]: value,
}))
}
function handleSubmit(e) {
e.preventDefault();
setLoading(true);
Inertia.post(window.route('login.attempt'), values)
.then(() => {
setLoading(false);
})
}
return (
<Layout title="Inicia sesión">
<div className="w-full flex items-center justify-center">
<div className="w-full max-w-5xl flex justify-center items-start z-10 font-sans text-sm">
<div className="w-2/3 text-white mt-6 mr-16">
<div className="h-16 mb-2 flex items-center">
<span className="uppercase font-bold ml-3 text-lg hidden xl:block">
Optima spark
</span>
</div>
<h1 className="text-5xl leading-tight pb-4">
Vuelve inteligente tus operaciones
</h1>
<p className="text-lg">
Recoge data de tus instalaciones de forma automatizada; accede a información histórica y en tiempo real
para que puedas analizar y tomar mejores decisiones para tu negocio.
</p>
<button type="submit" className="bg-yellow-600 w-40 hover:bg-blue-dark text-white font-semibold py-2 px-4 rounded mt-8 shadow-md">
Más información
</button>
</div>
<div className="w-1/3 flex flex-col">
<div className="bg-white text-gray-700 shadow-md rounded rounded-lg px-8 pt-6 pb-8 mb-4 flex flex-col">
<div className="w-full rounded-lg h-16 flex items-center justify-center">
<span className="uppercase font-bold text-lg">Acceder</span>
</div>
<form onSubmit={handleSubmit} className={`relative ${loading ? 'invisible' : 'visible'}`}>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-semibold mb-2" htmlFor="email">
Email
</label>
<input
id="email"
type="text"
className=" appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 outline-none focus:border-1 focus:border-yellow-500"
placeholder="Introduce tu e-mail.."
name="email"
value={values.email}
onChange={handleChange}
/>
{errors.email && <p className="text-red-500 text-xs italic">{ errors.email[0] }</p>}
</div>
<div className="mb-6">
<label className="block text-gray-700 text-sm font-semibold mb-2" htmlFor="password">
Contraseña
</label>
<input
className=" appearance-none border border-red rounded w-full py-2 px-3 text-gray-700 mb-3 outline-none focus:border-1 focus:border-yellow-500"
id="password"
name="password"
type="password"
placeholder="*********"
value={values.password}
onChange={handleChange}
/>
{errors.password && <p className="text-red-500 text-xs italic">{ errors.password[0] }</p>}
</div>
<div className="flex flex-col items-start justify-between">
<LoadingButton loading={loading} label='Iniciar sesión' />
<a className="font-semibold text-sm text-blue hover:text-blue-700 mt-4"
href="#">
<u>Olvidé mi contraseña</u>
</a>
</div>
<div
className={`absolute top-0 left-0 right-0 bottom-0 flex items-center justify-center ${!loading ? 'invisible' : 'visible'}`}
>
<div className="lds-ellipsis">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
</form>
</div>
<div className="w-full flex justify-center">
<a href="https://optimaee.com">
</a>
</div>
</div>
</div>
</div>
</Layout>
);
};
export default login;
To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. Whenever you call a setState variable, it will tell the component to re-render and run the code again.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.” The memory leak will happen if the API server or host took some time to respond and the component was unmounted before the response was received.
Memory leaks in React applications are primarily a result of not cancelling subscriptions made when a component was mounted before the component gets unmounted. These subscriptions could be a DOM Event listener, a WebSocket subscription, or even a request to an API.
Because it's the async promise call, so you must use a mutable reference variable (with useRef) to check already unmounted component for the next treatment of async response (avoiding memory leaks) :
Warning: Can't perform a React state update on an unmounted component.
Two React Hooks that you should use in this case : useRef
and useEffect
.
With useRef
, for example, the mutable variable _isMounted
is always pointed at the same reference in memory (not a local variable)
useRef is the go-to hook if mutable variable is needed. Unlike local variables, React makes sure same reference is returned during each render. If you want, it's the same with this.myVar in Class Component
Example :
const login = (props) => {
const _isMounted = useRef(true); // Initial value _isMounted = true
useEffect(() => {
return () => { // ComponentWillUnmount in Class Component
_isMounted.current = false;
}
}, []);
function handleSubmit(e) {
e.preventDefault();
setLoading(true);
ajaxCall = Inertia.post(window.route('login.attempt'), values)
.then(() => {
if (_isMounted.current) { // Check always mounted component
// continue treatment of AJAX response... ;
}
)
}
}
On the same occasion, let me explain you more information about React Hooks used here. Also, I will compare React Hooks in Functional Component (React >16.8) with the LifeCycle in Class Component.
useEffect : Most side-effects happen inside the hook. Examples of side effects are : data fetching, setting up a subscription, and manually changing the DOM React components. The useEffect replaces a lot of LifeCycles in Class Component (componentDidMount, componentDidUpate, componentWillUnmount)
useEffect(fnc, [dependency1, dependency2, ...]); // dependencies array argument is optional
Default behavior of useEffect runs both after the first render (like ComponentDidMount) and after every update render (like ComponentDidUpdate) if you don't have dependencies. It's like that : useEffect(fnc);
Giving array of dependencies to useEffect will change its lifecycle. In this example : useEffect will be called once after the first render and every time count changes
export default function () { const [count, setCount] = useState(0);
useEffect(fnc, [count]);
}
useEffect will run only once after the first render (like ComponentDidMount) if you put an empty array for dependency. It's like that : useEffect(fnc, []);
To prevent resource leaks, everything must be disposed when lifecycle of a hook ends (like ComponentWillUnmount). For example, with the empty array of dependency, the returned function will be called after component unmounts. It's like that :
useEffect(() => { return fnc_cleanUp; // fnc_cleanUp will cancel all subscriptions and asynchronous tasks (ex. : clearInterval) }, []);
useRef : returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.
Example : with the question above, we can't use a local variable here because it will be lost and re-initiated on each update render.
const login = (props) => {
let _isMounted= true; // it isn't good because of a local variable, so the variable will be lost and re-defined on every update render
useEffect(() => {
return () => {
_isMounted = false; // not good
}
}, []);
// ...
}
So, with combination of useRef and useEffect, we could completely cleanup memory leaks.
The good links that you could read more about the React Hooks are :
[EN] https://medium.com/@sdolidze/the-iceberg-of-react-hooks-af0b588f43fb
[FR] https://blog.soat.fr/2019/11/react-hooks-par-lexemple/
Before mutating the state, you should first check whether the component is still mounted.
As said above by @SanjiMika, when having an async action that causes this error, it means you are trying to mutate the component's state after it was un-mounted.
react-use
provides hooks for that, you've got 2 options:
option #1: useMountedState
// check if isMounted before changing any state
const isMounted = useMountedState();
useEffect(() => {
const asyncAction = executeAsyncAction();
asyncAction.then(result => {
if (isMounted) {
// It's safe to mutate state here
}
});
}, []);
option #2: useUnmountPromise
/* `resolveWhileMounted` wraps your promise, and returns a promise that will resolve
* only while the component is still mounted */
const resolveWhileMounted = useUnmountPromise();
useEffect(async () => {
const asyncAction = executeAsyncAction();
resolveWhileMounted(asyncAction).then(result => {
// It's safe to mutate state here
});
}, []);
You could use the 'cancelActiveVisits' method of Inertia
to cancel the active visit
in useEffect
cleanup hook.
So with this call the active visit
will be cancelled and state will not get updated.
useEffect(() => {
return () => {
Inertia.cancelActiveVisits(); //To cancel the active visit.
}
}, []);
if the Inertia
request get cancelled then it will return an empty response so you have to add an extra check to handle the empty response.
Add add catch block as well to handle any potential errors.
function handleSubmit(e) {
e.preventDefault();
setLoading(true);
Inertia.post(window.route('login.attempt'), values)
.then(data => {
if(data) {
setLoading(false);
}
})
.catch( error => {
console.log(error);
});
}
Alternate way (workaround)
You Could use useRef
to hold the status of the component and based on this you can update the state
.
Problem:
The warring is showing because the handleSubmit
is trying to update the state of the component even though component has unmounted form the dom.
Solution:
Set a flag to hold the status of the component
, if the component
is mounted
then the flag
value will be true
and if the component
is unmounted
the flag value will be false. So based on this we can update the state
.
For flag status we can use useRef
to hold a reference.
useRef
returns a mutable ref object whose.current
property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component. InuseEffect
return a function which will set the status of the component, if it is unmounted.
And then in useEffect
in the cleanup function we can set the flag to false.
useEffecr cleanup function
The
useEffect
hook allows using a cleanup function. Anytime the effect is no longer valid, for example when a component using that effect is unmounting, this function is called to clean everything up. In our case, we can set the flag to false.
Example:
let _componentStatus.current = useRef(true);
useEffect(() => {
return () => {
_componentStatus.current = false;
}
}, []);
And in handleSubmit we can check if the component is mounted or not and update the state based on this.
function handleSubmit(e) {
e.preventDefault();
setLoading(true);
Inertia.post(window.route('login.attempt'), values)
.then(() => {
if (_componentStatus.current) {
setLoading(false);
} else {
_componentStatus = null;
}
})
}
In else set the _componentStatus
to null to avoid any memory leaks.
Functional Based & Class Based BOTH 👇🏻
// function based METHOD 1 👇🏻
const unsubscribe = useRef();
useEffect(() => {
unsubscribe.current = setTimeout(() => {
// do something
}, 1000);
return () => {
clearTimeout(unsubscribe.current);
unsubscribe.current = null;
};
}, []);
// function based METHOD 2 👇🏻
const [news, setNews] = useState();
const unsubscribe = useRef();
useEffect(() => {
unsubscribe.current = true
axios.get('domain').then((result) => {
if (unsubscribe) {
setNews(result);
}
});
return () => {
unsubscribe.current = false;
};
}, []);
// class based METHOD 1 👇🏻
unsubscribe = null;
componentDidMount() {
this.unsubscribe = setTimeout(() => {
// do something
}, 1000);
}
componentWillUnmount() {
clearTimeout(this.unsubscribe);
this.unsubscribe = null;
}
// class based METHOD 4
unsubscribe = false;
constructor(props) {
super(props);
this.state = {
news: [],
};
}
componentDidMount() {
this.unsubscribe = true;
axios.get('domain').then((result) => {
if (this.unsubscribe) {
this.setState({
news: result.data.hits,
});
}
});
}
componentWillUnmount() {
this.unsubscribe = false;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With