-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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: fix rules of React violations #11863
base: main
Are you sure you want to change the base?
Conversation
🦋 Changeset detectedLatest commit: 297919f The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
size-limit report 📦
|
✅ Deploy Preview for apollo-client-docs ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
@@ -64,7 +62,7 @@ describe("mockSubscriptionLink", () => { | |||
</ApolloProvider> | |||
); | |||
|
|||
const numRenders = IS_REACT_18 ? 2 : results.length + 1; |
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 are now using useSyncExternalStore
, so nothing will autobatch here in React 18. Seeing how that confused people with subscriptions, that's probably a good thing.
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.
Good call out. We have a section on the hooks page about subscription autobatching: https://www.apollographql.com/docs/react/api/react/hooks/#usesubscription we should create a ticket to update that with a version range once we know which version this change will ship in (and honestly that disclaimer should be on the subscriptions page too)
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.
That's a very good point - what do you think about these changes? e249bad
(#11863)
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.
Also, see this idea: #11863 (comment)
fetchPolicy !== observable.__.fetchPolicy || | ||
!equal(variables, observable.__.variables)) && | ||
(typeof shouldResubscribe === "function" ? | ||
!!shouldResubscribe(options!) |
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.
shouldResubscribe
should now only execute if something is actually different - but since we're doing this in render, it will be executed every render if they are different, not only if these values changed.
setObservable( | ||
(observable = createSubscription( | ||
client, | ||
subscription, | ||
variables, | ||
fetchPolicy, | ||
context | ||
)) | ||
); |
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.
Calling setObservable
during render will prevent an awkward in-between render where you already passed in new variables/query/client and it still shows you results for the old one.
variables, | ||
}; | ||
observable.__.setResult(result); | ||
update(); |
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.
Idea: Generally we could think about a omitData
option that would remove data
from the result and skip calling this update
(=component rerendering) if only the data changes. That way, people could subscribe and trigger updates from their own onData
handlers as they please.
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.
Uh yeah, that's #10216 ^^
context: options?.context, | ||
}) | ||
); | ||
canResetObservableRef.current = false; |
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.
The ref here was a workaround around strictMode, but didn't account for any kind of concurrency and broke rules of React.
https://github.com/apollographql/apollo-client/pull/9707/files
Out of curiosity, does this fix #11165? |
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 didn't get through the whole review before I left, but figured I'd submit what I had for now. Feel free to take or leave it.
src/react/hooks/useSubscription.ts
Outdated
@@ -102,32 +112,24 @@ export function useSubscription< | |||
TVariables extends OperationVariables = OperationVariables, | |||
>( | |||
subscription: DocumentNode | TypedDocumentNode<TData, TVariables>, | |||
options?: SubscriptionHookOptions<NoInfer<TData>, NoInfer<TVariables>> | |||
options: SubscriptionHookOptions<NoInfer<TData>, NoInfer<TVariables>> = {} |
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.
options: SubscriptionHookOptions<NoInfer<TData>, NoInfer<TVariables>> = {} | |
options: SubscriptionHookOptions<NoInfer<TData>, NoInfer<TVariables>> = Object.create(null) |
I believe we've been using this version of an empty object throughout the project.
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.
Fair enough.
@@ -11,8 +11,6 @@ import { itAsync, MockSubscriptionLink } from "../../../../testing"; | |||
import { graphql } from "../../graphql"; | |||
import { ChildProps } from "../../types"; | |||
|
|||
const IS_REACT_18 = React.version.startsWith("18"); |
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.
🎉
((client !== observable.__.client || | ||
subscription !== observable.__.query || | ||
fetchPolicy !== observable.__.fetchPolicy || | ||
!equal(variables, observable.__.variables)) && |
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.
Since variables
is stable via useDeepMemo
, do we need the deep equality check here or can we rely on reference equality?
!equal(variables, observable.__.variables)) && | |
variables !== observable.__.variables) && |
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.
Hmmmmmmm... it's only memoized, so it's not guaranteed to be stable.
We could consider
!equal(variables, observable.__.variables)) && | |
variables !== observable.__.variables || !equal(variables, observable.__.variables)) && |
here, but it would add more bytes to save a little bit of speed.
I don't think it adds a lot of speed though, since a referential equality check will be the first thing equal
does anyways, so I'd leave it as it is.
src/react/hooks/useSubscription.ts
Outdated
let { skip, fetchPolicy, variables, shouldResubscribe, context } = options; | ||
variables = useDeepMemo(() => variables, [variables]); |
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.
let { skip, fetchPolicy, variables, shouldResubscribe, context } = options; | |
variables = useDeepMemo(() => variables, [variables]); | |
const { skip, fetchPolicy, shouldResubscribe, context } = options; | |
const variables = useDeepMemo(() => options.variables, [options.variables]); |
Perhaps a personal preference, but could we create the variables
variable like this? I find this a bit easier to remember that variables
references the memoized value easier than having to look at where its reassigned from. (and feel free to leave the first let
if you prefer... I know 'let' vs 'const' yada yada).
This is technical more bytes, so feel free to ignore this suggestion if you prefer the way it is.
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.
Perfectly fair :)
when will this be merged? can we integrate this #10216 to it as it is critical to decouple streaming data update vs react-render in a lot of use case |
@kjhuang-db we won't release this PR until 3.11 which is scheduled for early July. With a big refactor like this, we tend to want to put this in a minor release. The work to integrate the new option should likely be done in a separate PR to ensure those changes don't get buried along with the rest of this code, but it should absolutely start with the changes from this branch to avoid conflicts. |
@jerelmiller sounds good. curious since this change seems switching the state from local to externalStore, with such big refactor why it is not a major release?
will branch out from this PR and see if I can add on it: I would prefer this in OSS apollo to avoid a local "copy" in my org's codebase, thanks also a qq for @phryneas: as discussed on Discord, one concern of current
related: facebook/react#25191 (comment) (what a small world) |
@kjhuang-db answered in discord :) |
Let's address #10216 and #11165 in separate follow-up PRs. I've added both to the 3.11.0 milestone. |
Another one for #11511 - this one is essentially a rewrite.