Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 " +
Expand All @@ -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();
Copy link
Member

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.

Copy link
Contributor Author

@fidelman fidelman Feb 24, 2025

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:

  1. bf2f03e - replace useState with observableQuery.getCurrentResult(), commit text: Only trigger useState change when we really want it to
  2. 21f61ee#diff-72832f4a1f9e169a840a8d60938b0a66249d0dc384d1cf3124f6ca049747f63f - replace forceUpdate with a dedicated hook, commit text: [apollo-react-relay-duct-tape] Implement useRefetchableFragment
  3. 87cb259, added a comment // Unclear why, but this yields twice with the same results, so skip one., commit text: [ARRDT] Cleanup

My 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 forceUpdate triggers the hook again getting an actual value from the observableQuery.getCurrentResult();.

Here is how it looks without "skip". Just load and then the "Load more" button click

Screenshot 2025-02-24 173550

Here is how it looks w/ "skip" aka "old version":

Screenshot 2025-02-24 173609
Screenshot 2025-02-24 173836.

And here is the logs of the new version:
Screenshot 2025-02-24 174213
As you can see it is identical to the old version, this is why I think we are good to remove that.

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
Copy link
Contributor

Choose a reason for hiding this comment

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

We must keep this invariant, but trigger it if useFragment returns !result.complete

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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],
};
}
Loading
Loading