-
Notifications
You must be signed in to change notification settings - Fork 46.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
useSubscription hook #15022
useSubscription hook #15022
Conversation
You may ask author about ownership like you did with scheduler package. |
}); | ||
|
||
// If the source has changed since our last render, schedule an update with its current value. | ||
if (state.source !== source) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isnt this somewhat a side effect in render phase? i suppose it leads to a predictable result even if a particular render gets replayed, but shouldnt generally this be done inside useEffect?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. A side effect would be e.g. mutating a variable or calling a callback. This is just telling React to schedule some follow up work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is essentially following the pattern we recommend for derived state:
https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the clarification!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know you showed me this earlier, but I can't remember why this part is necessary given that source
is one of the deps in the effect below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the thing I'm guarding against here (which maybe my comment doesn't do a good job of clarifying) is this:
- Subscription added to source A
- Component renders with a new source, source B
- Source A emits an update
- Passive effect is invoked, removing subscription from A and adding to B
We need to ignore the update from A that happens after we get a new source (but before our passive effect is fired).
I tried to make this clear with all of the inline comments but maybe I could improve them somehow?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is didUnsubscribe
not sufficient for that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. Because in the case I mentioned above, we haven't unsubscribed yet (because the passive effect wasn't yet fired).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We always fire pending passive effects right at the beginning of setState
, to prevent this type of scenario
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah but I guess that would require closing over a mutable variable. Ok now I get it.
Yeah. If we decide to move forward with a package like this for a few derivative hooks, it may be worth reaching out to TJ about the package name. Too soon at the moment though. |
// If the value hasn't changed, no update is needed. | ||
// Return state as-is so React can bail out and avoid an unnecessary render. | ||
const value = getCurrentValue(source); | ||
if (prevState.value === value) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check is only necessary right after subscribing to a new source, correct? But it looks like you're checking every time the subscription produces a new value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check might be useful in two scenarios:
- Our manual call to
checkForUpdates()
in the passive effect body (a few lines below). - When we attach our subscription (since some sources, like rxjs, will auto-invoke a handler when attached).
I could use another local var (like didUnsubscribe
) to track this but that doesn't seem any better than this check, IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How are 1 and 2 different? I would expect that if you have 1 (the manual call) you don't need 2.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We sync-check in case we've missed an update (1). We also need to subscribe for updates to be notified of future updates (2). We wouldn't have to sync-check if all subscription sources auto-invoked our subscription callback, but that's not the case. Some do, some don't.
It occurs to me that // Instead of `source` argument
function useStore(store) {
return useSubscription({
source: store,
getCurrentValue() {
return store.getState(),
},
subscribe() {
return store.subscribe(),
}
});
}
// Use deps array, like we do with useEffect and useMemo
function useStore(store) {
return useSubscription(() => {
return {
getCurrentValue() {
return store.getState();
},
subscribe() {
return store.subscribe();
},
};
}, [store]);
}
Aside from consistency between APIs, this also lets you depend on multiple values changing, and you don't need to add an extra |
Hm. Interesting. No, I didn't really consider that variation. We wouldn't have Dan's awesome lint rule to back us up (since this is a derived hook). |
I can add an exception if it's a recommended one. |
@gaearon Is figuring out a heuristic for custom hooks on the roadmap? Seems important. (Although I don't have any ideas.) |
Okay. I'll update this PR with the proposed API change then. |
Updated! |
31af392
to
87f7c31
Compare
Based on the outcome of our chat this morning, I've reverted the dependencies array change in favor of the previous Back to you for review, @acdlite. |
b3e8a0a
to
140cdb8
Compare
Does the first example in the PR post need updating? I got confused for a second. |
Ah, yeah. I updated the Gist but forgot about the PR description example. |
subscribe: callback => {
source.addEventListener("change", handler);
return () => source.removeEventListener("change", handler);
} did you mean subscribe: handler => {
source.addEventListener("change", handler);
return () => source.removeEventListener("change", handler);
} in the PR description? (callback -> handler) |
return {...prevState, value}; | ||
}); | ||
}; | ||
const unsubscribe = subscribe(source, checkForUpdates); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do we need to pass source in here? wouldn't it already accessible at the call site?
export function useSelector(selector) {
const store = useContext(ReduxContext);
const subscription = useMemo(
() => ({
source: store,
subscribe: (store, handler) => {
// why do we need the store as argument here?
return store.subscribe(handler);
},
getCurrentValue: () => {
return selector(store.getState());
}
}),
[store]
);
return useSubscription(subscription);
}
140cdb8
to
64f0b44
Compare
I've updated this proposal again with a less redundant API that leans more heavily on |
I recently shared an example
useSubscription
hook as a gist. Like thecreateSubscription
class approach it was based on, this has a lot of subtle nuance– so it seems like maybe something that we should consider releasing an "official" version of (along with perhapsuseFetch
).Here is the hook and some unit tests for discussion purposes.
For now I've added it inside of a new private package
react-hooks
(even though that name is already taken). We can bikeshed a real name later, before releasing (assuming we actually decide to do so).Example usage: