Skip to content

Commit ba7e551

Browse files
rickdunkinsaihaj
andauthored
feat(typescript-react-apollo): Apollo Client useFragment hook (#483)
* Add Apollo useFragment hook wrappers config option * useFragment hooks codegen in React Apollo plugin * Add withFragmentHooks option in dev-test example * Support keyFields in generated useFragment hooks * Create loud-monkeys-yawn.md --------- Co-authored-by: Saihajpreet Singh <[email protected]>
1 parent 64c2c10 commit ba7e551

6 files changed

Lines changed: 190 additions & 2 deletions

File tree

.changeset/loud-monkeys-yawn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@graphql-codegen/typescript-react-apollo": minor
3+
---
4+
5+
Apollo Client `useFragment` hook

dev-test/codegen.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,15 @@ const config: CodegenConfig = {
4444
'./dev-test/githunt/types.reactApollo.hooks.tsx': {
4545
schema: './dev-test/githunt/schema.json',
4646
documents: './dev-test/githunt/**/*.graphql',
47-
plugins: ['typescript', 'typescript-operations', 'typescript-react-apollo'],
47+
plugins: [
48+
'typescript',
49+
'typescript-operations',
50+
{
51+
'typescript-react-apollo': {
52+
withFragmentHooks: true,
53+
},
54+
},
55+
],
4856
},
4957
'./dev-test/githunt/types.react-query.ts': {
5058
schema: './dev-test/githunt/schema.json',

dev-test/githunt/types.reactApollo.hooks.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,52 @@ export const FeedEntryFragmentDoc = gql`
389389
${VoteButtonsFragmentDoc}
390390
${RepoInfoFragmentDoc}
391391
`;
392+
export function useCommentsPageCommentFragment<F = { id: string }>(identifiers: F) {
393+
return Apollo.useFragment<CommentsPageCommentFragment>({
394+
fragment: CommentsPageCommentFragmentDoc,
395+
fragmentName: 'CommentsPageComment',
396+
from: {
397+
__typename: 'Comment',
398+
...identifiers,
399+
},
400+
});
401+
}
402+
export type CommentsPageCommentFragmentHookResult = ReturnType<
403+
typeof useCommentsPageCommentFragment
404+
>;
405+
export function useFeedEntryFragment<F = { id: string }>(identifiers: F) {
406+
return Apollo.useFragment<FeedEntryFragment>({
407+
fragment: FeedEntryFragmentDoc,
408+
fragmentName: 'FeedEntry',
409+
from: {
410+
__typename: 'Entry',
411+
...identifiers,
412+
},
413+
});
414+
}
415+
export type FeedEntryFragmentHookResult = ReturnType<typeof useFeedEntryFragment>;
416+
export function useRepoInfoFragment<F = { id: string }>(identifiers: F) {
417+
return Apollo.useFragment<RepoInfoFragment>({
418+
fragment: RepoInfoFragmentDoc,
419+
fragmentName: 'RepoInfo',
420+
from: {
421+
__typename: 'Entry',
422+
...identifiers,
423+
},
424+
});
425+
}
426+
export type RepoInfoFragmentHookResult = ReturnType<typeof useRepoInfoFragment>;
427+
export function useVoteButtonsFragment<F = { id: string }>(identifiers: F) {
428+
return Apollo.useFragment<VoteButtonsFragment>({
429+
fragment: VoteButtonsFragmentDoc,
430+
fragmentName: 'VoteButtons',
431+
from: {
432+
__typename: 'Entry',
433+
...identifiers,
434+
},
435+
});
436+
}
437+
export type VoteButtonsFragmentHookResult = ReturnType<typeof useVoteButtonsFragment>;
392438
export const OnCommentAddedDocument = gql`
393439
subscription onCommentAdded($repoFullName: String!) {
394440
commentAdded(repoFullName: $repoFullName) {

packages/plugins/typescript/react-apollo/src/config.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,31 @@ export interface ReactApolloRawPluginConfig extends RawClientSideBasePluginConfi
217217
* ```
218218
*/
219219
withMutationOptionsType?: boolean;
220+
221+
/**
222+
* @description Whether or not to include wrappers for Apollo's useFragment hook.
223+
* @default false
224+
*
225+
* @exampleMarkdown
226+
* ```ts filename="codegen.ts"
227+
* import type { CodegenConfig } from '@graphql-codegen/cli';
228+
*
229+
* const config: CodegenConfig = {
230+
* // ...
231+
* generates: {
232+
* 'path/to/file.ts': {
233+
* plugins: ['typescript', 'typescript-operations', 'typescript-react-apollo'],
234+
* config: {
235+
* withFragmentHooks: true
236+
* },
237+
* },
238+
* },
239+
* };
240+
* export default config;
241+
* ```
242+
*/
243+
withFragmentHooks?: boolean;
244+
220245
/**
221246
* @description Allows you to enable/disable the generation of docblocks in generated code.
222247
* Some IDE's (like VSCode) add extra inline information with docblocks, you can disable this feature if your preferred IDE does not.

packages/plugins/typescript/react-apollo/src/visitor.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface ReactApolloPluginConfig extends ClientSideBasePluginConfig {
2121
withHooks: boolean;
2222
withMutationFn: boolean;
2323
withRefetchFn: boolean;
24+
withFragmentHooks?: boolean;
2425
apolloReactCommonImportFrom: string;
2526
apolloReactComponentsImportFrom: string;
2627
apolloReactHocImportFrom: string;
@@ -62,6 +63,7 @@ export class ReactApolloVisitor extends ClientSideBaseVisitor<
6263
withHooks: getConfigValue(rawConfig.withHooks, true),
6364
withMutationFn: getConfigValue(rawConfig.withMutationFn, true),
6465
withRefetchFn: getConfigValue(rawConfig.withRefetchFn, false),
66+
withFragmentHooks: getConfigValue(rawConfig.withFragmentHooks, false),
6567
apolloReactCommonImportFrom: getConfigValue(
6668
rawConfig.apolloReactCommonImportFrom,
6769
rawConfig.reactApolloVersion === 2
@@ -192,10 +194,14 @@ export class ReactApolloVisitor extends ClientSideBaseVisitor<
192194
const baseImports = super.getImports();
193195
const hasOperations = this._collectedOperations.length > 0;
194196

195-
if (!hasOperations) {
197+
if (!hasOperations && !this.config.withFragmentHooks) {
196198
return baseImports;
197199
}
198200

201+
if (this.config.withFragmentHooks) {
202+
return [...baseImports, this.getApolloReactHooksImport(false), ...Array.from(this.imports)];
203+
}
204+
199205
return [...baseImports, ...Array.from(this.imports)];
200206
}
201207

@@ -583,4 +589,58 @@ export class ReactApolloVisitor extends ClientSideBaseVisitor<
583589
.filter(a => a)
584590
.join('\n');
585591
}
592+
593+
public get fragments(): string {
594+
const fragments = super.fragments;
595+
596+
if (this._fragments.length === 0 || !this.config.withFragmentHooks) {
597+
return fragments;
598+
}
599+
600+
const operationType = 'Fragment';
601+
602+
const hookFns: string[] = [fragments];
603+
604+
for (const fragment of this._fragments.values()) {
605+
if (fragment.isExternal) {
606+
continue;
607+
}
608+
609+
const nodeName = fragment.name ?? '';
610+
const suffix = this._getHookSuffix(nodeName, operationType);
611+
const fragmentName: string =
612+
this.convertName(nodeName, {
613+
suffix,
614+
useTypesPrefix: false,
615+
useTypesSuffix: false,
616+
}) + this.config.hooksSuffix;
617+
618+
const operationTypeSuffix: string = this.getOperationSuffix(fragmentName, operationType);
619+
620+
const operationResultType: string = this.convertName(nodeName, {
621+
suffix: operationTypeSuffix + this._parsedConfig.operationResultSuffix,
622+
});
623+
624+
const IDType = this.scalars.ID ?? 'string';
625+
626+
const hook = `export function use${fragmentName}<F = { id: ${IDType} }>(identifiers: F) {
627+
return ${this.getApolloReactHooksIdentifier()}.use${operationType}<${operationResultType}>({
628+
fragment: ${nodeName}${this.config.fragmentVariableSuffix},
629+
fragmentName: "${nodeName}",
630+
from: {
631+
__typename: "${fragment.onType}",
632+
...identifiers,
633+
},
634+
});
635+
}`;
636+
637+
const hookResults = [
638+
`export type ${fragmentName}HookResult = ReturnType<typeof use${fragmentName}>;`,
639+
];
640+
641+
hookFns.push([hook, hookResults].join('\n'));
642+
}
643+
644+
return hookFns.join('\n');
645+
}
586646
}

packages/plugins/typescript/react-apollo/tests/react-apollo.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ describe('React Apollo', () => {
7070
}
7171
`);
7272

73+
const fragmentDoc = parse(/* GraphQL */ `
74+
fragment RepositoryFields on Repository {
75+
full_name
76+
html_url
77+
owner {
78+
avatar_url
79+
}
80+
}
81+
`);
82+
7383
const validateTypeScript = async (
7484
output: Types.PluginOutput,
7585
testSchema: GraphQLSchema,
@@ -2725,6 +2735,40 @@ export function useListenToCommentsSubscription(baseOptions?: Apollo.Subscriptio
27252735
await validateTypeScript(content, schema, docs, {});
27262736
});
27272737

2738+
it('should import fragments from near operation file for useFragment', async () => {
2739+
const config: ReactApolloRawPluginConfig = {
2740+
documentMode: DocumentMode.external,
2741+
importDocumentNodeExternallyFrom: 'near-operation-file',
2742+
withComponent: false,
2743+
withHooks: true,
2744+
withHOC: false,
2745+
withFragmentHooks: true,
2746+
};
2747+
2748+
const docs = [{ location: 'path/to/document.graphql', document: fragmentDoc }];
2749+
2750+
const content = (await plugin(schema, docs, config, {
2751+
outputFile: 'graphql.tsx',
2752+
})) as Types.ComplexPluginOutput;
2753+
2754+
expect(content.prepend[0]).toBeSimilarStringTo(`import * as Apollo from '@apollo/client';`);
2755+
2756+
expect(content.content).toBeSimilarStringTo(`
2757+
export function useRepositoryFieldsFragment<F = { id: string }>(identifiers: F) {
2758+
return Apollo.useFragment<RepositoryFieldsFragment>({
2759+
fragment: RepositoryFieldsFragmentDoc,
2760+
fragmentName: "RepositoryFields",
2761+
from: {
2762+
__typename: "Repository",
2763+
...identifiers,
2764+
},
2765+
});
2766+
}
2767+
export type RepositoryFieldsFragmentHookResult = ReturnType<typeof useRepositoryFieldsFragment>;
2768+
`);
2769+
await validateTypeScript(content, schema, docs, {});
2770+
});
2771+
27282772
it('should import Operations from near operation file for useMutation', async () => {
27292773
const config: ReactApolloRawPluginConfig = {
27302774
documentMode: DocumentMode.external,

0 commit comments

Comments
 (0)