— JavaScript, React, RxJS — 4 min read
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:
setTimeout
or setInterval
This is just a warning and it is not a stopper for development, but as such it is showing that in our application code there may be some issues - for example, we can have a memory leak which can lead to performance issues.
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 we 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 detail about RxJS
as well.
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 workaround 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.
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 detail 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
.
RxJS
operatorWe described in an 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
.
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:
1import { Observable } from 'rxjs';2const useUnmount$ = (): Observable<void> => {};34export default useUnmount$;
Now we have our hook, but we should add the implementation. We should return Observable
and be able to emit values. We are going to use Subject
for this.
1import { Observable, Subject } from 'rxjs';2const useUnmount$ = (): Observable<void> => {3 const unmount$ = new Subject<void>();45 return unmount$;6};78export 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.
1import { Observable, Subject } from 'rxjs';2import { useEffect } from 'react';34const useUnmount$ = (): Observable<void> => {5 const unmount$ = new Subject<void>();67 useEffect(8 () => () => {9 // implicit return instead of wrapping in {} and using return10 unmount$.next();11 unmount$.complete();12 },13 [unmount$]14 );1516 return unmount$;17};1819export 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.
1import { Observable, Subject } from 'rxjs';2import { useEffect, useMemo } from 'react';34const useUnmount$ = (): Observable<void> => {5 const unmount$ = useMemo(() => new Subject<void>(), []);67 useEffect(8 () => () => {9 unmount$.next();10 unmount$.complete();11 },12 [unmount$]13 );1415 return unmount$;16};1718export 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.
1import { useCallback } from 'react';2import { from } from 'rxjs';3import { takeUntil } from 'rxjs/operators';45import useUnmount$ from './useUnmount';67const useRequest = () => {8 const unmount$ = useUnmount$();910 // from("response") should be replaced by your implementation returning Observable11 return useCallback(() => from('response').pipe(takeUntil(unmount$)), [unmount$]);12};1314export default useRequest;
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.
Thank you for reading this to the end 🙌 . If you enjoyed it and learned something new, support me by clicking the share button below to reach more people and/or give me a follow on Twitter where we can catch up. I am sharing some other tips, articles, and things I learn there.
If you didn't like the article or you have an idea for improvement, please reach out to me on Twitter and drop me a DM with feedback so I can improve and provide better content in the future 💪.