Skip to content

Commit

Permalink
feat(typescript-react-apollo): Apollo Client useFragment hook (#483)
Browse files Browse the repository at this point in the history
* 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 <saihajpreet.singh@gmail.com>
  • Loading branch information
rickdunkin and saihaj authored Feb 2, 2024
1 parent 64c2c10 commit ba7e551
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/loud-monkeys-yawn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@graphql-codegen/typescript-react-apollo": minor
---

Apollo Client `useFragment` hook
10 changes: 9 additions & 1 deletion dev-test/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,15 @@ const config: CodegenConfig = {
'./dev-test/githunt/types.reactApollo.hooks.tsx': {
schema: './dev-test/githunt/schema.json',
documents: './dev-test/githunt/**/*.graphql',
plugins: ['typescript', 'typescript-operations', 'typescript-react-apollo'],
plugins: [
'typescript',
'typescript-operations',
{
'typescript-react-apollo': {
withFragmentHooks: true,
},
},
],
},
'./dev-test/githunt/types.react-query.ts': {
schema: './dev-test/githunt/schema.json',
Expand Down
46 changes: 46 additions & 0 deletions dev-test/githunt/types.reactApollo.hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,52 @@ export const FeedEntryFragmentDoc = gql`
${VoteButtonsFragmentDoc}
${RepoInfoFragmentDoc}
`;
export function useCommentsPageCommentFragment<F = { id: string }>(identifiers: F) {
return Apollo.useFragment<CommentsPageCommentFragment>({
fragment: CommentsPageCommentFragmentDoc,
fragmentName: 'CommentsPageComment',
from: {
__typename: 'Comment',
...identifiers,
},
});
}
export type CommentsPageCommentFragmentHookResult = ReturnType<
typeof useCommentsPageCommentFragment
>;
export function useFeedEntryFragment<F = { id: string }>(identifiers: F) {
return Apollo.useFragment<FeedEntryFragment>({
fragment: FeedEntryFragmentDoc,
fragmentName: 'FeedEntry',
from: {
__typename: 'Entry',
...identifiers,
},
});
}
export type FeedEntryFragmentHookResult = ReturnType<typeof useFeedEntryFragment>;
export function useRepoInfoFragment<F = { id: string }>(identifiers: F) {
return Apollo.useFragment<RepoInfoFragment>({
fragment: RepoInfoFragmentDoc,
fragmentName: 'RepoInfo',
from: {
__typename: 'Entry',
...identifiers,
},
});
}
export type RepoInfoFragmentHookResult = ReturnType<typeof useRepoInfoFragment>;
export function useVoteButtonsFragment<F = { id: string }>(identifiers: F) {
return Apollo.useFragment<VoteButtonsFragment>({
fragment: VoteButtonsFragmentDoc,
fragmentName: 'VoteButtons',
from: {
__typename: 'Entry',
...identifiers,
},
});
}
export type VoteButtonsFragmentHookResult = ReturnType<typeof useVoteButtonsFragment>;
export const OnCommentAddedDocument = gql`
subscription onCommentAdded($repoFullName: String!) {
commentAdded(repoFullName: $repoFullName) {
Expand Down
25 changes: 25 additions & 0 deletions packages/plugins/typescript/react-apollo/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,31 @@ export interface ReactApolloRawPluginConfig extends RawClientSideBasePluginConfi
* ```
*/
withMutationOptionsType?: boolean;

/**
* @description Whether or not to include wrappers for Apollo's useFragment hook.
* @default false
*
* @exampleMarkdown
* ```ts filename="codegen.ts"
* import type { CodegenConfig } from '@graphql-codegen/cli';
*
* const config: CodegenConfig = {
* // ...
* generates: {
* 'path/to/file.ts': {
* plugins: ['typescript', 'typescript-operations', 'typescript-react-apollo'],
* config: {
* withFragmentHooks: true
* },
* },
* },
* };
* export default config;
* ```
*/
withFragmentHooks?: boolean;

/**
* @description Allows you to enable/disable the generation of docblocks in generated code.
* Some IDE's (like VSCode) add extra inline information with docblocks, you can disable this feature if your preferred IDE does not.
Expand Down
62 changes: 61 additions & 1 deletion packages/plugins/typescript/react-apollo/src/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface ReactApolloPluginConfig extends ClientSideBasePluginConfig {
withHooks: boolean;
withMutationFn: boolean;
withRefetchFn: boolean;
withFragmentHooks?: boolean;
apolloReactCommonImportFrom: string;
apolloReactComponentsImportFrom: string;
apolloReactHocImportFrom: string;
Expand Down Expand Up @@ -62,6 +63,7 @@ export class ReactApolloVisitor extends ClientSideBaseVisitor<
withHooks: getConfigValue(rawConfig.withHooks, true),
withMutationFn: getConfigValue(rawConfig.withMutationFn, true),
withRefetchFn: getConfigValue(rawConfig.withRefetchFn, false),
withFragmentHooks: getConfigValue(rawConfig.withFragmentHooks, false),
apolloReactCommonImportFrom: getConfigValue(
rawConfig.apolloReactCommonImportFrom,
rawConfig.reactApolloVersion === 2
Expand Down Expand Up @@ -192,10 +194,14 @@ export class ReactApolloVisitor extends ClientSideBaseVisitor<
const baseImports = super.getImports();
const hasOperations = this._collectedOperations.length > 0;

if (!hasOperations) {
if (!hasOperations && !this.config.withFragmentHooks) {
return baseImports;
}

if (this.config.withFragmentHooks) {
return [...baseImports, this.getApolloReactHooksImport(false), ...Array.from(this.imports)];
}

return [...baseImports, ...Array.from(this.imports)];
}

Expand Down Expand Up @@ -583,4 +589,58 @@ export class ReactApolloVisitor extends ClientSideBaseVisitor<
.filter(a => a)
.join('\n');
}

public get fragments(): string {
const fragments = super.fragments;

if (this._fragments.length === 0 || !this.config.withFragmentHooks) {
return fragments;
}

const operationType = 'Fragment';

const hookFns: string[] = [fragments];

for (const fragment of this._fragments.values()) {
if (fragment.isExternal) {
continue;
}

const nodeName = fragment.name ?? '';
const suffix = this._getHookSuffix(nodeName, operationType);
const fragmentName: string =
this.convertName(nodeName, {
suffix,
useTypesPrefix: false,
useTypesSuffix: false,
}) + this.config.hooksSuffix;

const operationTypeSuffix: string = this.getOperationSuffix(fragmentName, operationType);

const operationResultType: string = this.convertName(nodeName, {
suffix: operationTypeSuffix + this._parsedConfig.operationResultSuffix,
});

const IDType = this.scalars.ID ?? 'string';

const hook = `export function use${fragmentName}<F = { id: ${IDType} }>(identifiers: F) {
return ${this.getApolloReactHooksIdentifier()}.use${operationType}<${operationResultType}>({
fragment: ${nodeName}${this.config.fragmentVariableSuffix},
fragmentName: "${nodeName}",
from: {
__typename: "${fragment.onType}",
...identifiers,
},
});
}`;

const hookResults = [
`export type ${fragmentName}HookResult = ReturnType<typeof use${fragmentName}>;`,
];

hookFns.push([hook, hookResults].join('\n'));
}

return hookFns.join('\n');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ describe('React Apollo', () => {
}
`);

const fragmentDoc = parse(/* GraphQL */ `
fragment RepositoryFields on Repository {
full_name
html_url
owner {
avatar_url
}
}
`);

const validateTypeScript = async (
output: Types.PluginOutput,
testSchema: GraphQLSchema,
Expand Down Expand Up @@ -2725,6 +2735,40 @@ export function useListenToCommentsSubscription(baseOptions?: Apollo.Subscriptio
await validateTypeScript(content, schema, docs, {});
});

it('should import fragments from near operation file for useFragment', async () => {
const config: ReactApolloRawPluginConfig = {
documentMode: DocumentMode.external,
importDocumentNodeExternallyFrom: 'near-operation-file',
withComponent: false,
withHooks: true,
withHOC: false,
withFragmentHooks: true,
};

const docs = [{ location: 'path/to/document.graphql', document: fragmentDoc }];

const content = (await plugin(schema, docs, config, {
outputFile: 'graphql.tsx',
})) as Types.ComplexPluginOutput;

expect(content.prepend[0]).toBeSimilarStringTo(`import * as Apollo from '@apollo/client';`);

expect(content.content).toBeSimilarStringTo(`
export function useRepositoryFieldsFragment<F = { id: string }>(identifiers: F) {
return Apollo.useFragment<RepositoryFieldsFragment>({
fragment: RepositoryFieldsFragmentDoc,
fragmentName: "RepositoryFields",
from: {
__typename: "Repository",
...identifiers,
},
});
}
export type RepositoryFieldsFragmentHookResult = ReturnType<typeof useRepositoryFieldsFragment>;
`);
await validateTypeScript(content, schema, docs, {});
});

it('should import Operations from near operation file for useMutation', async () => {
const config: ReactApolloRawPluginConfig = {
documentMode: DocumentMode.external,
Expand Down

0 comments on commit ba7e551

Please sign in to comment.