diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index 2422d04f284..b72af461a1e 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -2251,7 +2251,7 @@ export interface UseFragmentOptions extends Omit> | string; + from: StoreObject | Reference | FragmentType> | string | null; // (undocumented) optimistic?: boolean; } diff --git a/.api-reports/api-report-react_hooks.api.md b/.api-reports/api-report-react_hooks.api.md index ee1ad74eb32..3fca18975da 100644 --- a/.api-reports/api-report-react_hooks.api.md +++ b/.api-reports/api-report-react_hooks.api.md @@ -2075,7 +2075,7 @@ export interface UseFragmentOptions extends Omit> | string; + from: StoreObject | Reference | FragmentType> | string | null; // (undocumented) optimistic?: boolean; } diff --git a/.api-reports/api-report-react_internal.api.md b/.api-reports/api-report-react_internal.api.md index 3b1b0176a38..9505c29f83b 100644 --- a/.api-reports/api-report-react_internal.api.md +++ b/.api-reports/api-report-react_internal.api.md @@ -2138,7 +2138,7 @@ interface UseFragmentOptions extends Omit> | string; + from: StoreObject | Reference | FragmentType> | string | null; // (undocumented) optimistic?: boolean; } diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index 21a585568a5..5058c56771a 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -2924,7 +2924,7 @@ export function useFragment(options: Us export interface UseFragmentOptions extends Omit, NoInfer_2>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit, "id" | "variables" | "returnPartialData"> { client?: ApolloClient; // (undocumented) - from: StoreObject | Reference | FragmentType> | string; + from: StoreObject | Reference | FragmentType> | string | null; // (undocumented) optimistic?: boolean; } diff --git a/.changeset/long-zoos-ring.md b/.changeset/long-zoos-ring.md new file mode 100644 index 00000000000..6c7d5072a1a --- /dev/null +++ b/.changeset/long-zoos-ring.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": minor +--- + +Allow `null` as a valid `from` value in `useFragment`. diff --git a/.size-limits.json b/.size-limits.json index c1d6e8cd5fb..91e88ff5ac3 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 41573, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34361 + "dist/apollo-client.min.cjs": 41601, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34359 } diff --git a/src/react/hooks/__tests__/useFragment.test.tsx b/src/react/hooks/__tests__/useFragment.test.tsx index 80e27f4a1ed..470f8a2358a 100644 --- a/src/react/hooks/__tests__/useFragment.test.tsx +++ b/src/react/hooks/__tests__/useFragment.test.tsx @@ -1646,6 +1646,95 @@ describe("useFragment", () => { } }); + it("allows `null` as valid `from` value without warning", async () => { + using _ = spyOnConsole("warn"); + + interface Fragment { + age: number; + } + + const fragment: TypedDocumentNode = gql` + fragment UserFields on User { + age + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + const { takeSnapshot } = renderHookToSnapshotStream( + () => useFragment({ fragment, from: null }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { data, complete } = await takeSnapshot(); + + expect(data).toEqual({}); + expect(complete).toBe(false); + } + + expect(console.warn).not.toHaveBeenCalled(); + }); + + it("properly handles changing from null to valid from value", async () => { + using _ = spyOnConsole("warn"); + + interface Fragment { + __typename: "User"; + id: string; + age: number; + } + + const fragment: TypedDocumentNode = gql` + fragment UserFields on User { + __typename + id + age + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + client.writeFragment({ + fragment, + data: { + __typename: "User", + id: "1", + age: 30, + }, + }); + + const { takeSnapshot, rerender } = renderHookToSnapshotStream( + ({ from }) => useFragment({ fragment, from }), + { + initialProps: { from: null as UseFragmentOptions["from"] }, + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { data, complete } = await takeSnapshot(); + + expect(data).toEqual({}); + expect(complete).toBe(false); + } + + rerender({ from: { __typename: "User", id: "1" } }); + + { + const { data, complete } = await takeSnapshot(); + + expect(data).toEqual({ __typename: "User", id: "1", age: 30 }); + expect(complete).toBe(true); + } + }); + describe("tests with incomplete data", () => { let cache: InMemoryCache, wrapper: React.FunctionComponent; const ItemFragment = gql` @@ -2327,7 +2416,7 @@ describe.skip("Type Tests", () => { test("UseFragmentOptions interface shape", () => { expectTypeOf>().branded.toEqualTypeOf<{ - from: string | StoreObject | Reference | FragmentType; + from: string | StoreObject | Reference | FragmentType | null; fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; optimistic?: boolean; diff --git a/src/react/hooks/useFragment.ts b/src/react/hooks/useFragment.ts index 61736c5eee3..dd96c0f6f52 100644 --- a/src/react/hooks/useFragment.ts +++ b/src/react/hooks/useFragment.ts @@ -25,7 +25,7 @@ export interface UseFragmentOptions Cache.ReadFragmentOptions, "id" | "variables" | "returnPartialData" > { - from: StoreObject | Reference | FragmentType> | string; + from: StoreObject | Reference | FragmentType> | string | null; // Override this field to make it optional (default: true). optimistic?: boolean; /** @@ -73,7 +73,10 @@ function _useFragment( // `stableOptions` and retrigger our subscription. If the cache identifier // stays the same between renders, we want to reuse the existing subscription. const id = React.useMemo( - () => (typeof from === "string" ? from : cache.identify(from)), + () => + typeof from === "string" ? from + : from === null ? null + : cache.identify(from), [cache, from] ); @@ -83,6 +86,16 @@ function _useFragment( // get the correct diff on the next render given new diffOptions const diff = React.useMemo(() => { const { fragment, fragmentName, from, optimistic = true } = stableOptions; + + if (from === null) { + return { + result: diffToResult({ + result: {} as TData, + complete: false, + }), + }; + } + const { cache } = client; const diff = cache.diff({ ...stableOptions, @@ -111,24 +124,28 @@ function _useFragment( React.useCallback( (forceUpdate) => { let lastTimeout = 0; - const subscription = client.watchFragment(stableOptions).subscribe({ - next: (result) => { - // Since `next` is called async by zen-observable, we want to avoid - // unnecessarily rerendering this hook for the initial result - // emitted from watchFragment which should be equal to - // `diff.result`. - if (equal(result, diff.result)) return; - diff.result = result; - // If we get another update before we've re-rendered, bail out of - // the update and try again. This ensures that the relative timing - // between useQuery and useFragment stays roughly the same as - // fixed in https://github.com/apollographql/apollo-client/pull/11083 - clearTimeout(lastTimeout); - lastTimeout = setTimeout(forceUpdate) as any; - }, - }); + + const subscription = + stableOptions.from === null ? + null + : client.watchFragment(stableOptions).subscribe({ + next: (result) => { + // Since `next` is called async by zen-observable, we want to avoid + // unnecessarily rerendering this hook for the initial result + // emitted from watchFragment which should be equal to + // `diff.result`. + if (equal(result, diff.result)) return; + diff.result = result; + // If we get another update before we've re-rendered, bail out of + // the update and try again. This ensures that the relative timing + // between useQuery and useFragment stays roughly the same as + // fixed in https://github.com/apollographql/apollo-client/pull/11083 + clearTimeout(lastTimeout); + lastTimeout = setTimeout(forceUpdate) as any; + }, + }); return () => { - subscription.unsubscribe(); + subscription?.unsubscribe(); clearTimeout(lastTimeout); }; },