-
Notifications
You must be signed in to change notification settings - Fork 45
Replace useFragment with Apollo's native #517
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
Open
fidelman
wants to merge
5
commits into
microsoft:main
Choose a base branch
from
fidelman:fidelman/replace-useFragment-with-apollos-native
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,22 +1,23 @@ | ||
| import { useEffect, useMemo } from "react"; | ||
| import invariant from "invariant"; | ||
| import { useForceUpdate } from "./useForceUpdate"; | ||
| import { useOverridenOrDefaultApolloClient } from "../../useOverridenOrDefaultApolloClient"; | ||
|
|
||
| import type { FragmentReference } from "./types"; | ||
| import type { CompiledArtefactModule } from "@graphitation/apollo-react-relay-duct-tape-compiler"; | ||
| import { Cache } from "@apollo/client/core"; | ||
| import type { | ||
| CompiledArtefactModule, | ||
| Metadata, | ||
| } from "@graphitation/apollo-react-relay-duct-tape-compiler"; | ||
| import { DocumentNode } from "@apollo/client/core"; | ||
| import { useFragment, UseFragmentOptions } from "@apollo/client"; | ||
| import { Kind, ValueNode } from "graphql"; | ||
|
|
||
| /** | ||
| * @param documents Compiled watch query document that is used to setup a narrow | ||
| * observable for just the data selected by the original fragment. | ||
| * @param fragmentReference A Node object that has a globally unique `id` field. | ||
| */ | ||
|
|
||
| export function useCompiledFragment( | ||
| documents: CompiledArtefactModule, | ||
| fragmentReference: FragmentReference, | ||
| ): object { | ||
| ) { | ||
| invariant( | ||
| fragmentReference, | ||
| "useFragment(): Expected metadata to have been extracted from " + | ||
|
|
@@ -28,73 +29,106 @@ export function useCompiledFragment( | |
| "useFragment(): Expected a `watchQueryDocument` to have been " + | ||
| "extracted. Did you forget to invoke the compiler?", | ||
| ); | ||
| const from = fragmentReference.id; | ||
| invariant( | ||
| typeof from === "string", | ||
| "useFragment(): Expected the fragment reference to have a string id.", | ||
| ); | ||
| invariant( | ||
| metadata, | ||
| "useFragment(): Expected metadata to have been extracted from " + | ||
| "the fragment. Did you forget to invoke the compiler?", | ||
| ); | ||
|
|
||
| const defaultVariables = getDefaultVariables(watchQueryDocument); | ||
| const variables = { | ||
| ...defaultVariables, | ||
| ...fragmentReference.__fragments, | ||
| }; | ||
|
|
||
| const doc: UseFragmentOptions<unknown, unknown> = { | ||
| fragment: getFragmentDocumentFromWatchQueryDocument( | ||
| watchQueryDocument, | ||
| metadata, | ||
| ), | ||
| fragmentName: metadata?.mainFragment?.name, | ||
| from, | ||
| variables, | ||
| }; | ||
|
|
||
| const client = useOverridenOrDefaultApolloClient(); | ||
| const forceUpdate = useForceUpdate(); | ||
| const result = useFragment(doc); | ||
|
|
||
| const observableQuery = useMemo( | ||
| () => | ||
| client.watchQuery({ | ||
| fetchPolicy: "cache-only", | ||
| query: watchQueryDocument, | ||
| returnPartialData: false, | ||
| variables: { | ||
| ...fragmentReference.__fragments, | ||
| id: fragmentReference.id, | ||
| __fragments: fragmentReference.__fragments, | ||
| }, | ||
| }), | ||
| [client, fragmentReference.id, fragmentReference.__fragments], | ||
| invariant( | ||
| result.complete, | ||
| "useFragment(): Missing data expected to be seeded by the execution query document. Please check your type policies and possibleTypes configuration. If only subset of properties is missing you might need to configure merge functions for non-normalized types.", | ||
| ); | ||
|
|
||
| useEffect(() => { | ||
| let skipFirst = true; | ||
| const subscription = observableQuery.subscribe( | ||
| () => { | ||
| // Unclear why, but this yields twice with the same results, so skip one. | ||
| if (skipFirst) { | ||
| skipFirst = false; | ||
| } else { | ||
| forceUpdate(); | ||
| } | ||
| }, | ||
| (error) => { | ||
| console.log(error); | ||
| }, | ||
| ); | ||
| return () => subscription.unsubscribe(); | ||
| }, [observableQuery]); | ||
| return result.data as object; | ||
| } | ||
|
|
||
| const result = observableQuery.getCurrentResult(); | ||
| if (result.partial) { | ||
| invariant( | ||
| false, | ||
| "useFragment(): Missing data expected to be seeded by the execution query document: %s. Please check your type policies and possibleTypes configuration. If only subset of properties is missing you might need to configure merge functions for non-normalized types.", | ||
| JSON.stringify( | ||
| // we need the cast because queryInfo and lastDiff are private but very useful for debugging | ||
| ( | ||
| observableQuery as unknown as { | ||
| queryInfo?: { lastDiff?: { diff?: Cache.DiffResult<unknown> } }; | ||
| } | ||
| ).queryInfo?.lastDiff?.diff?.missing?.map((e) => e.path), | ||
| ), | ||
| ); | ||
| } | ||
|
Comment on lines
-69
to
-82
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We must keep this invariant, but trigger it if
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks, fixed |
||
| let data = result.data; | ||
| if (metadata?.rootSelection) { | ||
| invariant( | ||
| data, | ||
| "useFragment(): Expected Apollo to respond with previously seeded data of the execution query document: %s. Did you configure your type policies and possibleTypes correctly? Check apollo-react-relay-duct-tape README for more details.", | ||
| JSON.stringify({ | ||
| selection: metadata.rootSelection, | ||
| mainFragment: metadata.mainFragment, | ||
| }), | ||
| ); | ||
| data = data[metadata.rootSelection]; | ||
| type DefaultValue = | ||
| | string | ||
| | number | ||
| | boolean | ||
| | { [key: string]: DefaultValue } | ||
| | DefaultValue[] | ||
| | undefined; | ||
|
|
||
| const extractValue = (node: ValueNode): DefaultValue => { | ||
| if (!node) return undefined; | ||
|
|
||
| switch (node.kind) { | ||
| case Kind.INT: | ||
| case Kind.FLOAT: | ||
| return Number(node.value); | ||
| case Kind.STRING: | ||
| case Kind.ENUM: | ||
| case Kind.BOOLEAN: | ||
| return node.value; | ||
| case Kind.LIST: | ||
| return node.values.map(extractValue); | ||
| case Kind.OBJECT: | ||
| return node.fields.reduce<Record<string, DefaultValue>>((obj, field) => { | ||
| obj[field.name.value] = extractValue(field.value); | ||
| return obj; | ||
| }, {}); | ||
| default: | ||
| return undefined; | ||
| } | ||
| }; | ||
|
|
||
| function getDefaultVariables(documentNode: DocumentNode) { | ||
| const variableDefinitions = documentNode.definitions | ||
| .filter((def) => def.kind === Kind.OPERATION_DEFINITION) | ||
| .flatMap((def) => def.variableDefinitions || []); | ||
|
|
||
| return variableDefinitions.reduce<Record<string, DefaultValue>>( | ||
| (acc, varDef) => { | ||
| if (varDef.defaultValue) { | ||
| acc[varDef.variable.name.value] = extractValue(varDef.defaultValue); | ||
| } | ||
| return acc; | ||
| }, | ||
| {}, | ||
| ); | ||
| } | ||
| function getFragmentDocumentFromWatchQueryDocument( | ||
| watchQueryDocument: DocumentNode, | ||
| metadata: Metadata, | ||
| ): DocumentNode { | ||
| const fragmentDefinition = watchQueryDocument.definitions.find((def) => { | ||
| if (def.kind === "FragmentDefinition") { | ||
| return def.name.value === metadata?.mainFragment?.name; | ||
| } | ||
| return false; | ||
| }); | ||
| invariant( | ||
| data, | ||
| "useFragment(): Expected Apollo to respond with previously seeded data of the execution query document. Did you configure your type policies and possibleTypes correctly? Check apollo-react-relay-duct-tape README for more details.", | ||
| fragmentDefinition, | ||
| "useFragment(): Expected a fragment definition to be found in the " + | ||
| "watch query document. Did you forget to invoke the compiler?", | ||
| ); | ||
| return data; | ||
| return { | ||
| kind: "Document", | ||
| definitions: [fragmentDefinition], | ||
| }; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 missing from the new implementation, and I do recall that I added this for a reason. It would be good to check the history for when I added this ability and see if there's a reason left in those commits.
Uh oh!
There was an error while loading. Please reload this page.
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.
@alloy TLDR; we do not need that in the new implementation
My explanation
The old implementation were introduced by 3 commits:
useStatewithobservableQuery.getCurrentResult(), commit text: Only trigger useState change when we really want it toMy assumption, correct me if you recall, The subscription was triggered twice (for unknown reason), so there is a logic to skip the first trigger and then
forceUpdatetriggers the hook again getting an actual value from theobservableQuery.getCurrentResult();.Here is how it looks without "skip". Just load and then the "Load more" button click
Here is how it looks w/ "skip" aka "old version":
And here is the logs of the new version:

As you can see it is identical to the old version, this is why I think we are good to remove that.