How to prevent React setState on unmounted component - a different approach
Martin Belev
Posted on May 16, 2020
If you are working with React, most probably you have already seen the below issues a lot.
Warning: Can only update a mounted or mounting component. This usually means you called setState, replaceState, or forceUpdate on an unmounted component. This is a no-op.
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.
They can be caused easily by not cleaning up when component unmounts or route is changed:
- using
setTimeout
orsetInterval
- asynchronous request to the server for fetching data when component mounts
- form submit handler sending request to the server
What is this indicating?
This is just a warning and it is not stopper for development, but as such it is showing that in our application code there may be some issues - for example we can have memory leak which can lead to performance issues.
What are we going to cover in this post?
Today we are going to look at a solution leveraging Observables
by using RxJS which will make us almost forget about the described issues. The solution is focused on making requests to the server, we are not going to cover setTimeout
/setInterval
usage. We are also going to be using hooks. I am going to provide more information about our use case and how ended up with this solution.
We are not going to look at other solutions like Cancellable Promises
, AbortController or isMounted
usage which is actually an antipattern - https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html. We are not going to get in details about RxJS
as well.
How do we ended up here?
For a long time we were using Promises for our requests. We started seeing the described warning more and more which was just showing us that we have to do something to solve it. I won't lie, at first we had a couple of usages of isMounted
which no one liked. We felt that it is not actually solving the problem but it is just a work around which prevented the call to setState
. We knew that this can't be the solution for us because it doesn't seem OK to write such additional code for every request that we are going to make.
The good thing though was that under the hood we were already using RxJS
and Observables
. We are working in a really big application so just removing the Promise
usage wasn't a solution. We were going to gradually remove the Promise
usage and start using only Observables
. We should mention that we can unsubscribe from Observable
, but again this is something that we should do for every request which is just not good enough...
I am feeling grateful and want to thank Jafar Husain for the wonderful course Asynchronous Programming in JavaScript (with Rx.js Observables) from which I learned so much and found the solution. The course is also available in Pluralsight - link.
What is the solution?
Different way to think about our problem
As Front-end developers, if we think more deeply about it, most of the things that we are doing can be described as a collection/stream of events happening over time. If we think about them as collection then this gives us new horizons because we know so many operations that we can do over collections (or at least I felt so). With a couple of operations like map
, filter
, reduce
, mergeMap
, concatMap
, flatMap
, switchMap
we can achieve so much. Jafar Husain is describing all of this in much greater details with great examples in his course - just give it a try.
So, let's think about our request(s) as one collection (Observable) - let's call this one A
. And our component unmounting as another - let's call it B
. We would like to somehow combine those two in such a way that A
should emit values until an event occurs in B
.
Choosing RxJS
operator
We described in abstract way what we want to achieve. Now let's look at some of the implementation details. We are using RxJS
which comes with a great number of operators that will solve most of our problems. When we look at the operators, takeUntil looks perfect for our use case - "Emits the values emitted by the source Observable until a notifier Observable emits a value.". This is exactly what we wanted so now we know that we are going to use takeUntil
.
Going for the implementation
We are going to implement a custom hook which will be used to solve our problem. Let's start with the basics and just declare the structure of our hook:
import { Observable } from "rxjs";
const useUnmount$ = (): Observable<void> => {};
export default useUnmount$;
Now we have our hook, but we should add the implementation. We should return Observable
and being able to emit values. We are going to use Subject
for this.
import { Observable, Subject } from "rxjs";
const useUnmount$ = (): Observable<void> => {
const unmount$ = new Subject<void>();
return unmount$;
};
export default useUnmount$;
Good, but we are not there yet. We know that unmount will happen only once so we can emit and complete after this happens. We are going to use useEffect
cleanup function to understand when the component is unmounted.
import { Observable, Subject } from "rxjs";
import { useEffect } from "react";
const useUnmount$ = (): Observable<void> => {
const unmount$ = new Subject<void>();
useEffect(
() => () => { // implicit return instead of wrapping in {} and using return
unmount$.next();
unmount$.complete();
},
[unmount$]
);
return unmount$;
};
export default useUnmount$;
It looks like we completed our implementation, but we are not yet. What is going to happen if the component where useUnmount$
is used unmounts? We are going to create another Subject
, emit and complete the previous one. We wouldn't want this behavior, but instead emitting only once when the component in which is used unmounts. useMemo
coming to the rescue here.
import { Observable, Subject } from "rxjs";
import { useEffect, useMemo } from "react";
const useUnmount$ = (): Observable<void> => {
const unmount$ = useMemo(() => new Subject<void>(), []);
useEffect(
() => () => {
unmount$.next();
unmount$.complete();
},
[unmount$]
);
return unmount$;
};
export default useUnmount$;
With this we completed the implementation of our custom hook, but we still have to plug it into our collection A
which is responsible for our requests. We will imagine that our request abstraction is returning Observable
. And now the only thing left is to use the useUnmount$
hook.
import { useCallback } from "react";
import { from } from "rxjs";
import { takeUntil } from "rxjs/operators";
import useUnmount$ from "./useUnmount";
const useRequest = () => {
const unmount$ = useUnmount$();
// from("response") should be replaced by your implementation returning Observable
return useCallback(() => from("response").pipe(takeUntil(unmount$)), [
unmount$,
]);
};
export default useRequest;
Conclusion
Observables
can come in handy in many ways. It is a topic worth learning about and I believe it is going to be used more and more in the future. In combination with hooks, IMO we had come up with a very clean solution. It is saving us the cognitive load to think about cleaning up after each request that is made. I think this is a great win because there is one thing less to think/worry about while developing or reviewing a PR.
Posted on May 16, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.