From 7e4e2610263120a160a556853b5be26eaac29e26 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 14 Nov 2024 15:30:14 -0700 Subject: [PATCH] Allow `null` as valid `from` value in `useFragment` --- .../hooks/__tests__/useFragment.test.tsx | 36 +++++++++++- src/react/hooks/useFragment.ts | 55 ++++++++++++------- 2 files changed, 71 insertions(+), 20 deletions(-) diff --git a/src/react/hooks/__tests__/useFragment.test.tsx b/src/react/hooks/__tests__/useFragment.test.tsx index 80e27f4a1ed..910199a84e9 100644 --- a/src/react/hooks/__tests__/useFragment.test.tsx +++ b/src/react/hooks/__tests__/useFragment.test.tsx @@ -1646,6 +1646,40 @@ 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(); + }); + describe("tests with incomplete data", () => { let cache: InMemoryCache, wrapper: React.FunctionComponent; const ItemFragment = gql` @@ -2327,7 +2361,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); }; },