From 54c4d2f3c719654e38e537ec38f1cb415c7c3f58 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 17 Mar 2023 12:11:49 -0600 Subject: [PATCH] Ensure `refetch`, `fetchMore`, and `subscribeToMore` functions returned by `useSuspenseQuery` are referentially stable between renders (#10656) * Ensure refetch, fetchMore, and subscribeToMore are referentially stable as data is updated * Add changeset --- .changeset/silver-radios-chew.md | 5 ++ .../hooks/__tests__/useSuspenseQuery.test.tsx | 37 ++++++++ src/react/hooks/useSuspenseQuery.ts | 86 +++++++++++++------ 3 files changed, 103 insertions(+), 25 deletions(-) create mode 100644 .changeset/silver-radios-chew.md diff --git a/.changeset/silver-radios-chew.md b/.changeset/silver-radios-chew.md new file mode 100644 index 00000000000..4b869d0329b --- /dev/null +++ b/.changeset/silver-radios-chew.md @@ -0,0 +1,5 @@ +--- +'@apollo/client': patch +--- + +Ensure `refetch`, `fetchMore`, and `subscribeToMore` functions returned by `useSuspenseQuery` are referentially stable between renders, even as `data` is updated. diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index cb1ff2ff65d..7e6f1a8127e 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -487,6 +487,43 @@ describe('useSuspenseQuery', () => { expect(result.current).toBe(previousResult); }); + it('ensures refetch, fetchMore, and subscribeToMore are referentially stable even after result data has changed', async () => { + const { query, mocks } = useSimpleQueryCase(); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); + + const { result } = renderSuspenseHook(() => useSuspenseQuery(query), { + client, + }); + + await waitFor(() => { + expect(result.current).toMatchObject({ + ...mocks[0].result, + error: undefined, + }); + }); + + const previousResult = result.current; + + client.writeQuery({ + query, + data: { greeting: 'Updated cache greeting' }, + }); + + await waitFor(() => { + expect(result.current.data).toEqual({ + greeting: 'Updated cache greeting', + }); + }); + + expect(result.current.fetchMore).toBe(previousResult.fetchMore); + expect(result.current.refetch).toBe(previousResult.refetch); + expect(result.current.subscribeToMore).toBe(previousResult.subscribeToMore); + }); + it('enables canonical results when canonizeResults is "true"', async () => { interface Result { __typename: string; diff --git a/src/react/hooks/useSuspenseQuery.ts b/src/react/hooks/useSuspenseQuery.ts index cdea052bf36..db372410ec2 100644 --- a/src/react/hooks/useSuspenseQuery.ts +++ b/src/react/hooks/useSuspenseQuery.ts @@ -37,11 +37,26 @@ export interface UseSuspenseQueryResult< client: ApolloClient; data: TData; error: ApolloError | undefined; - fetchMore: ObservableQueryFields['fetchMore']; - refetch: ObservableQueryFields['refetch']; - subscribeToMore: ObservableQueryFields['subscribeToMore']; + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; + subscribeToMore: SubscribeToMoreFunction; } +type FetchMoreFunction< + TData, + TVariables extends OperationVariables +> = ObservableQueryFields['fetchMore']; + +type RefetchFunction< + TData, + TVariables extends OperationVariables +> = ObservableQueryFields['refetch']; + +type SubscribeToMoreFunction< + TData, + TVariables extends OperationVariables +> = ObservableQueryFields['subscribeToMore']; + const SUPPORTED_FETCH_POLICIES: WatchQueryFetchPolicy[] = [ 'cache-first', 'network-only', @@ -147,34 +162,55 @@ export function useSuspenseQuery_experimental< }; }, []); + const fetchMore: FetchMoreFunction = useCallback( + (options) => { + const promise = observable.fetchMore(options); + + suspenseCache.add(query, watchQueryOptions.variables, { + promise, + observable, + }); + + return promise; + }, + [observable] + ); + + const refetch: RefetchFunction = useCallback( + (variables) => { + const promise = observable.refetch(variables); + + suspenseCache.add(query, watchQueryOptions.variables, { + promise, + observable, + }); + + return promise; + }, + [observable] + ); + + const subscribeToMore: SubscribeToMoreFunction = + useCallback((options) => observable.subscribeToMore(options), [observable]); + return useMemo(() => { return { client, data: result.data, error: errorPolicy === 'ignore' ? void 0 : toApolloError(result), - fetchMore: (options) => { - const promise = observable.fetchMore(options); - - suspenseCache.add(query, watchQueryOptions.variables, { - promise, - observable, - }); - - return promise; - }, - refetch: (variables?: Partial) => { - const promise = observable.refetch(variables); - - suspenseCache.add(query, watchQueryOptions.variables, { - promise, - observable, - }); - - return promise; - }, - subscribeToMore: (options) => observable.subscribeToMore(options), + fetchMore, + refetch, + subscribeToMore, }; - }, [client, result, observable, errorPolicy]); + }, [ + client, + fetchMore, + refetch, + result, + observable, + errorPolicy, + subscribeToMore, + ]); } function validateOptions(options: WatchQueryOptions) {