'React unsubscribe from RxJS subscription in useEffect
A quick question regarding RxJS in useEffect hooks
I've noticed the React community uses this unsubscribe pattern with RxJS:
useEffect(()=>{
const sub = interval(10).subscribe()
return ()=>sub.unsubscribe()
})
I'm curious if this is due to convention/code clarity, or if I'm overlooking something. I would imagine the following would be simpler:
useEffect(()=> interval(10).subscribe().unsubscribe)
However, I could be overlooking something.
Edit: View selected answer. "This" is bound on method call, rather than on subscription instantiation. As a result, unsubscribe fails due to the "this" object not referring to the interval subscription, but rather the useEffect callback environment. Thanks to both contributors. Here is an example of the useEffect hook failing: codesandbox.io/s/morning-bush-7b3m6h?file=/src/App.js
Solution 1:[1]
This here:
useEffect(()=>{
const sub = interval(10).subscribe();
return () => sub.unsubscribe();
});
could be re-written as:
useEffect(()=>{
const sub = interval(10).subscribe();
function unsub(){
sub.unsubscribe();
}
return unsub;
});
The key thing to notice is that you're returning a function back to React. unsub
isn't called right away, it's called later when the component unmounts.
In fact, you can return arbitrary code to be run later:
useEffect(() => {
/******
* Code that gets called when
* effect is run
******/
return () => { // <- start of cleanup function
/******
* Code that gets called to
* clean up the effect later
******/
} // <- end of cleanup function
});
The problem
I'll rewrite your solution to make talking about the problem clearer. This is semantically equivalent, I've just introduced an intermediate variable.
useEffect(() =>
const sub = interval(10).subscribe();
return sub.unsubscribe;
);
The the question most clearly boils down to: What are the differences between these values? Under which circumstances (if any) will one fail while the other does not.
sub.unsubscribe
() => sub.unsubscribe()
If unsubscribe is a function (isn't bound to an instance of a class/object because it doesn't contain the this keyword), then the two are semantically equivalent.
The issue is that unsubscribe is not actually a function. It's a method on an subscription object. Because of this, the first value above is an unbound method where this
is undefined. The moment the method attempts to use its context (this
), JavaScript will throw an error.
To make sure that unsubscribe
gets called as a method you could do this:
useEffect(() => {
const sub = interval(10).subscribe();
return sub.unsubscribe.bind(sub);
});
You have one less level of indirection this way, though it looks roughly the same.
Furthermore, I would recommend against using bind
in most cases. Methods, functions, anonymous lambda functions, and attributes containing any of these three as values all behave differently on various edge cases.
As far as I know, () => a.b()
may be needlessly wrapping a function, but will not fail. Plus JIT will optimize this fairly well 99.9% of cases.
Where a.b.bind(a)
will fail on a previously bound method, but be optimized 100% of the time. I wouldn't use bind
unless it's necessary (and it rarely is)
Update:
Just a quick aside: I use function here to denote a callable block of code which doesn't rely on a context (Doesn't have an object that it references using the this
keyword) and a method to denote a callable block of code that DOES rely on some context.
If you prefer other terminology, that's fine. Swap out the words as you read them, I won't take offense, promise :)
Solution 2:[2]
Beware of this
The short answer is that the unsubscribe
function uses the this
keyword and was designed to be called as a property of the subscription object (e.g., as subscription.unsubscribe()) rather than "on its own" (e.g., as just
unsubscribe()`).
What is this
?
In JavaScript (in addition to the arguments passed to it and the closed-over values in scope) a function may use the this
keyword to access a special contextual value. Originally, this
was meant to be equal to the "receiver", or the object that the function was being called from. For example, if you set a.f = f
and b.f = f
with any function f
, you can call a.f()
and this
will be equal to a
or call b.f()
and this
will be equal to b
. If you call f()
by itself, this
will be undefined
.
How does that matter?
In this case, interval(10).subscribe()
returns a Subscription object with an unsubscribe
function that expects this
to be that same subscription object. Particularly, this line of the current source code for unsubscribe
checks this.closed
to avoid re-closing subscriptions that have already been closed.
So, while you're permitted to pull interval(10).subscribe().unsubscribe
away and call it without a receiver, you will get an error like "Cannot read properties of undefined (reading 'closed')".
What can I do about it?
Unfortunately, it's not always clear when this
is being used in a function that you didn't write. It's best to avoid isolating a function that came from an object (e.g., const f = obj.func;
) unless you're sure it's this
-free. But if a function is already "on its own", you don't usually have to worry about this
.
However, there are the following workarounds:
Wrap it in a new function that always calls it with the expected receiver. This is what your example does:
() => sub.unsubscribe()
.Create a "bound" copy of the function. Functions are objects too, and available to each function is the
bind
function that can be used to sort of hard-code the value ofthis
. If you writeconst unsub = sub.unsubscribe.bind(this);
, callingunsub
will always call theunsubscribe
function withsub
as the value ofthis
. This practically very similar to option 1 in that it creates a new function, but this is more streamlined to its purpose.Use an "arrow" function. This isn't something consumers can do, but the
rxjs
authors could have written it this way:unsubscribe = () => { ... }
. Then you could pull the function out and use it by itself with no errors. This is because "arrow" functions — functions created with the() => ...
syntax — have special behavior: they always preservethis
from wherever they were created. So when constructing anew Subscription()
, it gets a dedicatedunsubscribe
function whosethis
value will always equal the constructed subscription object. But since theunsubscribe
function is currently declared without the arrow syntax, it uses the typicalthis
-equals-receiver behavior. Furthermore, since it's declared directly in the body of theSubscription
class (rather than a value that gets assigned to the property) it's also part of theSubscription
prototype and not part of the subscription object itself. Since it's shared across all instances,this
can't refer to any specific instance but must look at the receiver.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 |