Skip to content
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

Implement a @nonreactive directive for selectively skipping reactive comparisons of query result subtrees #10722

Merged
merged 18 commits into from
Apr 5, 2023

Conversation

benjamn
Copy link
Member

@benjamn benjamn commented Apr 4, 2023

As foretold in #8694 (comment), and building on work started in #10237, this PR adds support for a new query directive called @nonreactive, which can be used to mark query fields or fragment spreads, indicating changes to the data contained within the subtrees marked @nonreactive should not trigger rerendering, allowing parent components to fetch child data without rerendering themselves every time something changes about one/some of the children (assuming the child data is marked @nonreactive in the parent query).

This selective insensitivity of parent components to certain child data was originally planned to be a feature of useBackgroundQuery, but that hook has turned into something else, and @nonreactive seems like a better solution for the original problem anyway.

Since @nonreactive is implemented at a point in the query pipeline where we were already performing a deep equality check (isDifferentFromLastResult in ObservableQuery), and since we "compare" @nonreactive subtrees by ignoring them completely (cheap!), the use of @nonreactive has the potential to allow much faster comparisons.

However, because this behavior is slightly different from the previous deep equality check (for example, the previous implementation was sensitive to extraneous fields in the result not mentioned by the query), the new behavior (using compareResultsUsingQuery) is only enabled when the query contains at least one @nonreactive directive. Since no one is using the @nonreactive directive yet, all existing result comparisons should continue to work exactly as before.

I recommend reviewing this PR commit by commit, and reading the commit messages, since they explain several bug fixes and refactorings to support the @nonreactive implementation.

benjamn added 8 commits April 4, 2023 13:58
This commit modifies the `isDifferentFromLastResult` logic in
`ObservableQuery` so it skips over `@nonreactive` fields (thereby
ignoring those subtrees of the query) whenever the `@nonreactive`
directive is present in the query.

When the `@nonreactive` directive is not present in the query,
`isDifferentFromLastResult` continues using the `equal` function from
`@wry/equality` to compare the results, which ensures the `@nonreactive`
behavior is opt-in, preserving backwards compatibility.

This functionality builds on the `compareResultsUsingQuery` predicate
function introduced in PR #10237.
The hasRemoveDirective logic was broken because it did not enforce that
the _matching_ directive config has a `config.remove` property.

This led to the inappropriate removal of directives without
`config.remove`, in situations when any other configs are present with
`config.remove` set to `true`, even if those other configs do not match
any directives found on the current AST node.
Faced with the prospect of performing a third whole-document traversal
to remove the `@nonreactive` directive, in addition to the traversals
already responsible for removing `@client` and `@connection`, I decided
it was time to use the full power of `removeDirectivesFromDocument` to
remove all three directives in one traversal.
These changes allow the latest equivalent result for queries using the
`@nonreactive` directive to be stored in this.last.result, rather than
being discarded in favor of an older (equivalent but different) result.
@benjamn benjamn self-assigned this Apr 4, 2023
@changeset-bot

This comment was marked as resolved.

@benjamn benjamn changed the title Implement and test a client-only @nonreactive directive Implement a @nonreactive directive for selectively skipping reactive comparisons of query result subtrees Apr 4, 2023
@benjamn benjamn added this to the Release 3.8 milestone Apr 4, 2023
Comment on lines 315 to 323
const resultIsDifferent =
this.queryManager.transform(query).hasNonreactiveDirective
? !compareResultsUsingQuery(
query,
this.last.result,
newResult,
this.variables,
)
: !equal(this.last.result, newResult);
Copy link
Member Author

@benjamn benjamn Apr 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where we handle the backwards compatibility logic of using compareResultsUsingQuery only when the query contains one or more @nonreactive directives.

Despite appearances, this.queryManager.transform(query).hasNonreactiveDirective is cached after the first time query is encountered, so we do not have to keep scanning the query for @nonreactive directives every time this code runs.

Comment on lines +308 to +317
it.each<TypedDocumentNode<{ list: Item[] }>>([
// This query uses a basic field-level @nonreactive directive.
gql`
query GetItems {
list {
id
text @nonreactive
}
}
`,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love how easy it is to add variations of test cases using it.each!

Comment on lines +621 to +625
const serverQuery = removeDirectivesFromDocument([
removeClientFields ? { name: 'client', remove: true } : {},
{ name: 'connection' },
{ name: 'nonreactive' },
], transformed);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like @client and @connection, the @nonreactive directive is removed from queries before they are sent to the server, though (unlike @client) only the @nonreactive directive is removed, preserving the field/fragment it was annotating (because we want all the data from the server, even if the client chooses not to broadcast it).

Thanks to 3030094, all three of these removals can be performed in the same traversal of the AST!

Copy link
Member

@jerelmiller jerelmiller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really great stuff! I had a few suggestions on some additional tests and structuring, but overall I'm really excited about this. Thanks for continuing to work on this particular feature!

src/core/__tests__/compareResults.ts Outdated Show resolved Hide resolved
src/core/__tests__/compareResults.ts Outdated Show resolved Hide resolved
src/core/__tests__/compareResults.ts Outdated Show resolved Hide resolved
src/core/__tests__/compareResults.ts Outdated Show resolved Hide resolved
src/core/__tests__/compareResults.ts Outdated Show resolved Hide resolved
src/core/compareResults.ts Outdated Show resolved Hide resolved
src/core/compareResults.ts Outdated Show resolved Hide resolved
src/core/compareResults.ts Outdated Show resolved Hide resolved
src/core/__tests__/compareResults.ts Outdated Show resolved Hide resolved
@alessbell
Copy link
Contributor

/release:pr

@github-actions
Copy link
Contributor

github-actions bot commented Apr 5, 2023

A new release has been made for this PR. You can install it with npm i @apollo/client@0.0.0-pr-10722-20230405145145.

Copy link
Contributor

@alessbell alessbell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excited to try this out! 🎉🎉

Created #10728 to track the docs changes

Copy link
Member Author

@benjamn benjamn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright @jerel and @alessbell, I think I've addressed all the feedback except for the "testing-library/no-wait-for-multiple-assertions": "off" change. Please take another look!

src/core/__tests__/compareResults.ts Outdated Show resolved Hide resolved
src/core/compareResults.ts Outdated Show resolved Hide resolved
src/core/compareResults.ts Outdated Show resolved Hide resolved
src/core/compareResults.ts Outdated Show resolved Hide resolved
src/core/__tests__/compareResults.ts Outdated Show resolved Hide resolved
src/core/__tests__/compareResults.ts Outdated Show resolved Hide resolved
src/core/__tests__/compareResults.ts Outdated Show resolved Hide resolved
src/core/__tests__/compareResults.ts Outdated Show resolved Hide resolved
src/core/compareResults.ts Outdated Show resolved Hide resolved
src/core/__tests__/compareResults.ts Outdated Show resolved Hide resolved
Copy link
Member

@jerelmiller jerelmiller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm super stoked about this 🎉 . I'm excited that we'll be able to apply it for any useX hook that renders queries. Nice work 🔥 🔥

@benjamn benjamn added 💡 idea 🧞‍♂️ enhancement 🧪 has-tests 🥚 backwards-compatible for PRs that do not introduce any breaking changes 🎬 directives Concerning GraphQL directives like `@defer`, `@live`, `@skip` and friends labels Apr 5, 2023
@benjamn benjamn merged commit 2df5b77 into release-3.8 Apr 5, 2023
@benjamn benjamn deleted the @nonreactive branch April 5, 2023 21:26
@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 6, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
🥚 backwards-compatible for PRs that do not introduce any breaking changes 🎬 directives Concerning GraphQL directives like `@defer`, `@live`, `@skip` and friends 🧞‍♂️ enhancement 🧪 has-tests
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants