From 6baf020b76d069b1fd09c8029176083320e4c347 Mon Sep 17 00:00:00 2001 From: Mark Polak Date: Wed, 6 Mar 2024 13:04:26 +0100 Subject: [PATCH 1/5] Make fragment ref nullable --- packages/nova-react/package.json | 1 + .../nova-react/src/graphql/hooks.test.tsx | 291 +++++++++++++++++- packages/nova-react/src/graphql/hooks.ts | 98 +++++- packages/nova-react/src/graphql/types.ts | 30 +- yarn.lock | 15 + 5 files changed, 418 insertions(+), 17 deletions(-) diff --git a/packages/nova-react/package.json b/packages/nova-react/package.json index 9fc7822..26285eb 100644 --- a/packages/nova-react/package.json +++ b/packages/nova-react/package.json @@ -23,6 +23,7 @@ "devDependencies": { "@graphitation/graphql-js-tag": "^0.9.0", "@testing-library/react": "^12.0.0", + "@testing-library/react-hooks": "^8.0.1", "@types/invariant": "^2.2.35", "@types/jest": "^29.2.0", "@types/react": "^17.0.2", diff --git a/packages/nova-react/src/graphql/hooks.test.tsx b/packages/nova-react/src/graphql/hooks.test.tsx index 031d07a..6ae23b9 100644 --- a/packages/nova-react/src/graphql/hooks.test.tsx +++ b/packages/nova-react/src/graphql/hooks.test.tsx @@ -4,6 +4,7 @@ import React from "react"; import { render, screen } from "@testing-library/react"; +import { renderHook } from "@testing-library/react-hooks"; import { NovaGraphQLProvider } from "./nova-graphql-provider"; import type { NovaGraphQL } from "@nova/types"; @@ -18,6 +19,9 @@ import { import type { GraphQLTaggedNode } from "./taggedNode"; import type { FragmentRefs } from "./types"; +type NotNull = null extends T ? false : true; +type NotUndefined = undefined extends T ? false : true; + describe(useLazyLoadQuery, () => { it("ensures an implementation is supplied", () => { const graphql: NovaGraphQL = {}; @@ -67,7 +71,7 @@ describe(useLazyLoadQuery, () => { expect(graphql.useLazyLoadQuery).toHaveBeenCalledWith( query, {}, - { context: { callerInfo: "subject-with-query" } } + { context: { callerInfo: "subject-with-query" } }, ); expect(screen.getByText("some-data")).toBeDefined(); }); @@ -152,9 +156,87 @@ describe(useFragment, () => { useFragment(fragment, opaqueFragmentRef); void _; }); + + it("allows null or undefined to be passed as the fragment ref", () => { + const fragment = {} as unknown as GraphQLTaggedNode; + + const { result, rerender } = renderHook< + { fragmentRef: null | undefined }, + null | undefined + >(({ fragmentRef }) => useFragment(fragment, fragmentRef), { + wrapper: ({ children }) => ( + {children} + ), + initialProps: { + fragmentRef: null, + }, + }); + + expect(result.current).toBeNull(); + + rerender({ fragmentRef: undefined }); + + expect(result.current).toBeUndefined(); + }); + + it("return type can be `null`, `undefined` or data", () => { + type SomeFragment$data = { someKey: string }; + type SomeFragment$key = { + readonly " $data"?: SomeFragment$data; + readonly " $fragmentRefs": FragmentRefs<"SomeFragment">; + }; + + const fragment = {} as unknown as GraphQLTaggedNode; + const opaqueFragmentRef = {} as unknown as SomeFragment$key | null; + + () => { + const data = useFragment(fragment, opaqueFragmentRef); + + type ExpectedReturnType = typeof data; + + const _: ExpectedReturnType = null; + const __: ExpectedReturnType = undefined; + const ___: ExpectedReturnType = { someKey: "some-data" }; + + // Workaround for TS complaining about unused variables + void _, __, ___; + }; + }); + + it("return type does not include `null` or `undefined`", () => { + type SomeFragment$data = { someKey: string }; + type SomeFragment$key = { + readonly " $data"?: SomeFragment$data; + readonly " $fragmentRefs": FragmentRefs<"SomeFragment">; + }; + + const fragment = {} as unknown as GraphQLTaggedNode; + const opaqueFragmentRef = {} as unknown as SomeFragment$key; + + () => { + const data = useFragment(fragment, opaqueFragmentRef); + + type ExpectedReturnType = typeof data; + + const _: NotNull = true; + const __: NotUndefined = true; + const ___: ExpectedReturnType = { someKey: "some-data" }; + + // Workaround for TS complaining about unused variables + void _, __, ___; + }; + }); }); describe(useRefetchableFragment, () => { + beforeEach(() => { + jest.restoreAllMocks(); + + jest.spyOn(console, "error").mockImplementation(() => { + /* noop */ + }); + }); + it("uses the host's hook, if provided", () => { const graphql: NovaGraphQL = { useRefetchableFragment: jest.fn(() => [ @@ -207,9 +289,118 @@ describe(useRefetchableFragment, () => { `"Expected host to provide a useRefetchableFragment hook"`, ); }); + + it("supports passing null as reference to the fragment", () => { + const { result } = renderHook( + () => { + const fragment = {} as unknown as GraphQLTaggedNode; + const fragmentRef = null; + + return useRefetchableFragment(fragment, fragmentRef); + }, + { + wrapper: ({ children }) => ( + [ref, jest.fn()], + }} + > + {children} + + ), + }, + ); + + expect(result.current[0]).toBeNull(); + }); + + it("return type does not include null or undefined when the ref is not null", () => { + const { result } = renderHook( + () => { + type SomeFragment$data = { someKey: string }; + type SomeFragment$key = { + readonly " $data"?: SomeFragment$data; + readonly " $fragmentRefs": FragmentRefs<"SomeFragment">; + }; + + const fragment = {} as unknown as GraphQLTaggedNode; + const opaqueFragmentRef = {} as unknown as SomeFragment$key; + + return useRefetchableFragment(fragment, opaqueFragmentRef); + }, + { + wrapper: ({ children }) => ( + [ref, jest.fn()], + }} + > + {children} + + ), + }, + ); + + type ExpectedReturnType = (typeof result.current)[0]; + + const _: NotNull = true; + const __: NotUndefined = true; + const ___: ExpectedReturnType = { someKey: "some-data " }; + + // Workaround for TS complaining about unused variables + void _, __, ___; + }); + + it("supports null and undefined as result types when the key can be null or undefined", () => { + const { result } = renderHook( + () => { + type SomeFragment$data = { someKey: string }; + type SomeFragment$key = { + readonly " $data"?: SomeFragment$data; + readonly " $fragmentRefs": FragmentRefs<"SomeFragment">; + }; + + const fragment = {} as unknown as GraphQLTaggedNode; + const opaqueFragmentRef = {} as unknown as + | SomeFragment$key + | null + | undefined; + + return useRefetchableFragment(fragment, opaqueFragmentRef); + }, + { + wrapper: ({ children }) => ( + [ref, jest.fn()], + }} + > + {children} + + ), + }, + ); + + type ExpectedReturnType = (typeof result.current)[0]; + + const _: NotNull = false; + const __: NotUndefined = false; + const ___: ExpectedReturnType = { someKey: "some-data " }; + + // Workaround for TS complaining about unused variables + void _, __, ___; + }); }); describe(usePaginationFragment, () => { + beforeEach(() => { + jest.restoreAllMocks(); + + jest.spyOn(console, "error").mockImplementation(() => { + /* noop */ + }); + }); + it("uses the host's hook, if provided", () => { const mockedResponse = { data: { @@ -272,6 +463,104 @@ describe(usePaginationFragment, () => { `"Expected host to provide a usePaginationFragment hook"`, ); }); + + it("does not include null or undefined in the return type when the key is defined.", () => { + const graphql: NovaGraphQL = { + usePaginationFragment: jest.fn().mockImplementation(() => ({ + data: {}, + })), + }; + + type SomeFragment$data = { someKey: string }; + + const { result } = renderHook( + () => { + type SomeFragment$key = { + readonly " $data"?: SomeFragment$data; + readonly " $fragmentRefs": FragmentRefs<"SomeFragment">; + }; + + const fragment = {} as unknown as GraphQLTaggedNode; + const opaqueFragmentRef = {} as unknown as SomeFragment$key; + + return usePaginationFragment(fragment, opaqueFragmentRef); + }, + { + wrapper: ({ children }) => ( + + {children} + + ), + }, + ); + + const _: SomeFragment$data = result.current.data; + void _; + }); + + it("allows null to be passed as a fragment ref and returns null or undefined", () => { + const graphql: NovaGraphQL = { + usePaginationFragment: jest.fn().mockImplementation(() => ({ + data: {}, + })), + }; + + type SomeFragment$data = { someKey: string }; + + const { result } = renderHook( + () => { + type SomeFragment$key = { + readonly " $data"?: SomeFragment$data; + readonly " $fragmentRefs": FragmentRefs<"SomeFragment">; + }; + + const fragment = {} as unknown as GraphQLTaggedNode; + const opaqueFragmentRef = {} as unknown as + | SomeFragment$key + | null + | undefined; + + return usePaginationFragment(fragment, opaqueFragmentRef); + }, + { + wrapper: ({ children }) => ( + + {children} + + ), + }, + ); + + const _: SomeFragment$data | null | undefined = result.current.data; + void _; + }); + + it("allows null to be passed as a fragment ref and returns null or undefined", () => { + const graphql: NovaGraphQL = { + usePaginationFragment: jest.fn().mockImplementation(() => ({ + data: {}, + })), + }; + + const { result } = renderHook( + () => { + const fragment = {} as unknown as GraphQLTaggedNode; + const fragmentRef = null; + + return usePaginationFragment(fragment, fragmentRef); + }, + { + wrapper: ({ children }) => ( + + {children} + + ), + }, + ); + + const _: null | undefined = result.current.data; + void _; + }); }); describe(useSubscription, () => { diff --git a/packages/nova-react/src/graphql/hooks.ts b/packages/nova-react/src/graphql/hooks.ts index de7eee9..36e30ce 100644 --- a/packages/nova-react/src/graphql/hooks.ts +++ b/packages/nova-react/src/graphql/hooks.ts @@ -60,7 +60,7 @@ import type { export function useLazyLoadQuery( query: GraphQLTaggedNode, variables: TQuery["variables"], - options?: { fetchPolicy?: "cache-first"; context?: TQuery["context"] } + options?: { fetchPolicy?: "cache-first"; context?: TQuery["context"] }, ): { error?: Error; data?: TQuery["response"] } { const graphql = useNovaGraphQL(); invariant( @@ -134,10 +134,23 @@ export function useLazyLoadQuery( * fragment. * @returns The data corresponding to the field selections. */ +export function useFragment( + fragmentInput: GraphQLTaggedNode, + fragmentRef: null | undefined, // This overload makes sure we do not get `unknown` as the return type. +): null | undefined; export function useFragment( fragmentInput: GraphQLTaggedNode, fragmentRef: TKey, -): KeyTypeData { +): KeyTypeData; +export function useFragment( + fragmentInput: GraphQLTaggedNode, + fragmentRef: TKey | null | undefined, +): KeyTypeData | null | undefined; + +export function useFragment( + fragmentInput: GraphQLTaggedNode, + fragmentRef: TKey | null | undefined, +): KeyTypeData | null | undefined { return ( useNovaGraphQL().useFragment?.(fragmentInput, fragmentRef) || fragmentRef ); @@ -153,13 +166,42 @@ export function useFragment( * fragment. * @returns The data corresponding to the field selections and a function to perform the refetch. */ +export function useRefetchableFragment< + TQuery extends OperationType, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _TKey extends KeyType, +>( + fragmentInput: GraphQLTaggedNode, + fragmentRef: null | undefined, +): [data: null | undefined, refetch: RefetchFn]; export function useRefetchableFragment< TQuery extends OperationType, TKey extends KeyType, >( fragmentInput: GraphQLTaggedNode, fragmentRef: TKey, -): [data: KeyTypeData, refetch: RefetchFn] { +): [data: KeyTypeData, refetch: RefetchFn]; +export function useRefetchableFragment< + TQuery extends OperationType, + TKey extends KeyType, +>( + fragmentInput: GraphQLTaggedNode, + fragmentRef: TKey | null | undefined, +): [ + data: KeyTypeData | null | undefined, + refetch: RefetchFn, +]; + +export function useRefetchableFragment< + TQuery extends OperationType, + TKey extends KeyType, +>( + fragmentInput: GraphQLTaggedNode, + fragmentRef: TKey | null | undefined, +): [ + data: KeyTypeData | null | undefined, + refetch: RefetchFn, +] { const graphql = useNovaGraphQL(); invariant( graphql.useRefetchableFragment, @@ -178,6 +220,23 @@ export function useRefetchableFragment< * fragment. * @returns The data corresponding to the field selections and functions to deal with pagination. */ + +export function usePaginationFragment< + TQuery extends OperationType, + TKey extends null | undefined, +>( + fragmentInput: GraphQLTaggedNode, + fragmentRef: null | undefined, +): { + data: null | undefined; + loadNext: PaginationFn; + loadPrevious: PaginationFn; + hasNext: boolean; + hasPrevious: boolean; + isLoadingNext: boolean; + isLoadingPrevious: boolean; + refetch: RefetchFn; +}; export function usePaginationFragment< TQuery extends OperationType, TKey extends KeyType, @@ -193,6 +252,39 @@ export function usePaginationFragment< isLoadingNext: boolean; isLoadingPrevious: boolean; refetch: RefetchFn; +}; +export function usePaginationFragment< + TQuery extends OperationType, + TKey extends KeyType, +>( + fragmentInput: GraphQLTaggedNode, + fragmentRef: TKey | null | undefined, +): { + data: KeyTypeData | null | undefined; + loadNext: PaginationFn; + loadPrevious: PaginationFn; + hasNext: boolean; + hasPrevious: boolean; + isLoadingNext: boolean; + isLoadingPrevious: boolean; + refetch: RefetchFn; +}; + +export function usePaginationFragment< + TQuery extends OperationType, + TKey extends KeyType, +>( + fragmentInput: GraphQLTaggedNode, + fragmentRef: TKey | null | undefined, +): { + data: KeyTypeData | null | undefined; + loadNext: PaginationFn; + loadPrevious: PaginationFn; + hasNext: boolean; + hasPrevious: boolean; + isLoadingNext: boolean; + isLoadingPrevious: boolean; + refetch: RefetchFn; } { const graphql = useNovaGraphQL(); invariant( diff --git a/packages/nova-react/src/graphql/types.ts b/packages/nova-react/src/graphql/types.ts index c35e2d8..0ad9f33 100644 --- a/packages/nova-react/src/graphql/types.ts +++ b/packages/nova-react/src/graphql/types.ts @@ -3,7 +3,7 @@ export interface Variables { } export interface Context { - [name: string]: any + [name: string]: any; } export interface OperationType { @@ -21,11 +21,13 @@ export interface _RefType { " $refType": Ref; } -export type _FragmentRefs = { - " $fragmentRefs": FragmentRefs; -} | { - " $fragmentSpreads": FragmentRefs; -} +export type _FragmentRefs = + | { + " $fragmentRefs": FragmentRefs; + } + | { + " $fragmentSpreads": FragmentRefs; + }; // This is used in the actual artifacts to define the various fragment references a container holds. export type FragmentRefs = { @@ -43,13 +45,15 @@ export type FragmentRef = Fragment extends _RefType export type FragmentReference = unknown; -export type KeyType = Readonly<{ - " $data"?: TData; - " $fragmentRefs": FragmentReference; -}> | Readonly<{ - " $data"?: TData; - " $fragmentSpreads": FragmentReference; -}>; +export type KeyType = + | Readonly<{ + " $data"?: TData; + " $fragmentRefs": FragmentReference; + }> + | Readonly<{ + " $data"?: TData; + " $fragmentSpreads": FragmentReference; + }>; export type KeyTypeData< TKey extends KeyType, diff --git a/yarn.lock b/yarn.lock index c3fb565..00274d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3014,6 +3014,14 @@ lodash "^4.17.15" redent "^3.0.0" +"@testing-library/react-hooks@^8.0.1": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12" + integrity sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-boundary "^3.1.0" + "@testing-library/react@^12.0.0": version "12.1.1" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.1.tgz#e693943aa48d0190099acdc3928a751d73bcf7d5" @@ -9098,6 +9106,13 @@ react-element-to-jsx-string@^15.0.0: is-plain-object "5.0.0" react-is "18.1.0" +react-error-boundary@^3.1.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" + integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== + dependencies: + "@babel/runtime" "^7.12.5" + react-inspector@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-6.0.1.tgz#1a37f0165d9df81ee804d63259eaaeabe841287d" From 960ca0bc9fc05825e0e3e1394b02869c02c15b0b Mon Sep 17 00:00:00 2001 From: Mark Polak Date: Wed, 6 Mar 2024 13:11:15 +0100 Subject: [PATCH 2/5] Update test name --- packages/nova-react/src/graphql/hooks.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nova-react/src/graphql/hooks.test.tsx b/packages/nova-react/src/graphql/hooks.test.tsx index 6ae23b9..2d2041c 100644 --- a/packages/nova-react/src/graphql/hooks.test.tsx +++ b/packages/nova-react/src/graphql/hooks.test.tsx @@ -498,7 +498,7 @@ describe(usePaginationFragment, () => { void _; }); - it("allows null to be passed as a fragment ref and returns null or undefined", () => { + it("allows fragment ref, null, or undefined to be passed as a fragment ref and returns data, null, or undefined", () => { const graphql: NovaGraphQL = { usePaginationFragment: jest.fn().mockImplementation(() => ({ data: {}, @@ -535,7 +535,7 @@ describe(usePaginationFragment, () => { void _; }); - it("allows null to be passed as a fragment ref and returns null or undefined", () => { + it("allows null or undefined to be passed as a fragment ref and returns null or undefined", () => { const graphql: NovaGraphQL = { usePaginationFragment: jest.fn().mockImplementation(() => ({ data: {}, From ed90b844e96b80cc1d4f346e7fc5ca5474ceae43 Mon Sep 17 00:00:00 2001 From: Mark Polak Date: Tue, 19 Mar 2024 11:13:18 +0100 Subject: [PATCH 3/5] Change files --- .../@nova-react-c10c9e36-4cda-4e41-bc32-85c8d2031bdb.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@nova-react-c10c9e36-4cda-4e41-bc32-85c8d2031bdb.json diff --git a/change/@nova-react-c10c9e36-4cda-4e41-bc32-85c8d2031bdb.json b/change/@nova-react-c10c9e36-4cda-4e41-bc32-85c8d2031bdb.json new file mode 100644 index 0000000..1777537 --- /dev/null +++ b/change/@nova-react-c10c9e36-4cda-4e41-bc32-85c8d2031bdb.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Make fragment ref nullable", + "packageName": "@nova/react", + "email": "mark@thedutchies.com", + "dependentChangeType": "patch" +} From 24fa55880a1282a6ea3c45802ae3c442f604572d Mon Sep 17 00:00:00 2001 From: Mark Polak Date: Tue, 19 Mar 2024 11:15:20 +0100 Subject: [PATCH 4/5] Clarify type names --- packages/nova-react/src/graphql/hooks.test.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/nova-react/src/graphql/hooks.test.tsx b/packages/nova-react/src/graphql/hooks.test.tsx index 2d2041c..5238c81 100644 --- a/packages/nova-react/src/graphql/hooks.test.tsx +++ b/packages/nova-react/src/graphql/hooks.test.tsx @@ -19,8 +19,8 @@ import { import type { GraphQLTaggedNode } from "./taggedNode"; import type { FragmentRefs } from "./types"; -type NotNull = null extends T ? false : true; -type NotUndefined = undefined extends T ? false : true; +type IsNotNull = null extends T ? false : true; +type IsNotUndefined = undefined extends T ? false : true; describe(useLazyLoadQuery, () => { it("ensures an implementation is supplied", () => { @@ -218,8 +218,8 @@ describe(useFragment, () => { type ExpectedReturnType = typeof data; - const _: NotNull = true; - const __: NotUndefined = true; + const _: IsNotNull = true; + const __: IsNotUndefined = true; const ___: ExpectedReturnType = { someKey: "some-data" }; // Workaround for TS complaining about unused variables @@ -343,8 +343,8 @@ describe(useRefetchableFragment, () => { type ExpectedReturnType = (typeof result.current)[0]; - const _: NotNull = true; - const __: NotUndefined = true; + const _: IsNotNull = true; + const __: IsNotUndefined = true; const ___: ExpectedReturnType = { someKey: "some-data " }; // Workaround for TS complaining about unused variables @@ -383,8 +383,8 @@ describe(useRefetchableFragment, () => { type ExpectedReturnType = (typeof result.current)[0]; - const _: NotNull = false; - const __: NotUndefined = false; + const _: IsNotNull = false; + const __: IsNotUndefined = false; const ___: ExpectedReturnType = { someKey: "some-data " }; // Workaround for TS complaining about unused variables From ab276b2061c26f9ee249f1bbcce5f9bdcc56ec38 Mon Sep 17 00:00:00 2001 From: Mark Polak Date: Wed, 20 Mar 2024 08:53:39 +0100 Subject: [PATCH 5/5] Fixes from PR comments --- .../nova-react/src/graphql/hooks.test.tsx | 81 ++++++++++++++----- packages/nova-react/src/graphql/hooks.ts | 3 +- 2 files changed, 63 insertions(+), 21 deletions(-) diff --git a/packages/nova-react/src/graphql/hooks.test.tsx b/packages/nova-react/src/graphql/hooks.test.tsx index 5238c81..ea3a78e 100644 --- a/packages/nova-react/src/graphql/hooks.test.tsx +++ b/packages/nova-react/src/graphql/hooks.test.tsx @@ -179,7 +179,7 @@ describe(useFragment, () => { expect(result.current).toBeUndefined(); }); - it("return type can be `null`, `undefined` or data", () => { + it("returns a value that can be `null`, `undefined` or data", () => { type SomeFragment$data = { someKey: string }; type SomeFragment$key = { readonly " $data"?: SomeFragment$data; @@ -189,21 +189,26 @@ describe(useFragment, () => { const fragment = {} as unknown as GraphQLTaggedNode; const opaqueFragmentRef = {} as unknown as SomeFragment$key | null; - () => { - const data = useFragment(fragment, opaqueFragmentRef); + const { result } = renderHook( + () => useFragment(fragment, opaqueFragmentRef), + { + wrapper: ({ children }) => ( + {children} + ), + }, + ); - type ExpectedReturnType = typeof data; + type ExpectedReturnType = typeof result.current; - const _: ExpectedReturnType = null; - const __: ExpectedReturnType = undefined; - const ___: ExpectedReturnType = { someKey: "some-data" }; + const _: ExpectedReturnType = null; + const __: ExpectedReturnType = undefined; + const ___: ExpectedReturnType = { someKey: "some-data" }; - // Workaround for TS complaining about unused variables - void _, __, ___; - }; + // Workaround for TS complaining about unused variables + void _, __, ___; }); - it("return type does not include `null` or `undefined`", () => { + it("returns a value that can not be `null` or `undefined` when the fragment ref is not `null` or `undefined`", () => { type SomeFragment$data = { someKey: string }; type SomeFragment$key = { readonly " $data"?: SomeFragment$data; @@ -213,18 +218,26 @@ describe(useFragment, () => { const fragment = {} as unknown as GraphQLTaggedNode; const opaqueFragmentRef = {} as unknown as SomeFragment$key; - () => { - const data = useFragment(fragment, opaqueFragmentRef); + const { result } = renderHook( + () => useFragment(fragment, opaqueFragmentRef), + { + wrapper: ({ children }) => ( + {children} + ), + initialProps: { + fragmentRef: opaqueFragmentRef, + }, + }, + ); - type ExpectedReturnType = typeof data; + type ExpectedReturnType = typeof result.current; - const _: IsNotNull = true; - const __: IsNotUndefined = true; - const ___: ExpectedReturnType = { someKey: "some-data" }; + const _: IsNotNull = true; + const __: IsNotUndefined = true; + const ___: ExpectedReturnType = { someKey: "some-data" }; - // Workaround for TS complaining about unused variables - void _, __, ___; - }; + // Workaround for TS complaining about unused variables + void _, __, ___; }); }); @@ -290,6 +303,34 @@ describe(useRefetchableFragment, () => { ); }); + it("supports passing null as reference to the fragment", () => { + const { result } = renderHook( + () => { + const fragment = {} as unknown as GraphQLTaggedNode; + + return useRefetchableFragment(fragment, null); + }, + { + wrapper: ({ children }) => ( + [ref, jest.fn()], + }} + > + {children} + + ), + }, + ); + + const [data] = result.current; + + const _: null | undefined = data; + void _; + + expect(result.current[0]).toBeNull(); + }); + it("supports passing null as reference to the fragment", () => { const { result } = renderHook( () => { diff --git a/packages/nova-react/src/graphql/hooks.ts b/packages/nova-react/src/graphql/hooks.ts index 36e30ce..2092ef6 100644 --- a/packages/nova-react/src/graphql/hooks.ts +++ b/packages/nova-react/src/graphql/hooks.ts @@ -136,7 +136,7 @@ export function useLazyLoadQuery( */ export function useFragment( fragmentInput: GraphQLTaggedNode, - fragmentRef: null | undefined, // This overload makes sure we do not get `unknown` as the return type. + fragmentRef: null | undefined, ): null | undefined; export function useFragment( fragmentInput: GraphQLTaggedNode, @@ -223,6 +223,7 @@ export function useRefetchableFragment< export function usePaginationFragment< TQuery extends OperationType, + // eslint-disable-next-line @typescript-eslint/no-unused-vars TKey extends null | undefined, >( fragmentInput: GraphQLTaggedNode,