From 6db44e8aac20ee191912e81b638b640f3adcea20 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 18 Jun 2025 17:17:56 +0200 Subject: [PATCH 01/10] wip --- src/core/ObservableQuery.ts | 49 +++++++++++++++++------------------ src/core/watchQueryOptions.ts | 20 ++++++++++---- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index 929bfbbb41e..fedc16f6657 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -708,29 +708,30 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, public fetchMore< TFetchData = TData, TFetchVars extends OperationVariables = TVariables, - >( - fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: ( - previousQueryResult: Unmasked, - options: { - fetchMoreResult: Unmasked; - variables: TFetchVars; - } - ) => Unmasked; - } - ): Promise> { + >({ + query, + variables, + context, + errorPolicy, + fetchPolicy, + updateQuery, + }: FetchMoreQueryOptions & { + updateQuery?: ( + previousQueryResult: Unmasked, + options: { + fetchMoreResult: Unmasked; + variables: NoInfer; + } + ) => Unmasked; + }): Promise> { const combinedOptions = { - ...(fetchMoreOptions.query ? fetchMoreOptions : ( - { - ...this.options, - query: this.options.query, - ...fetchMoreOptions, - variables: { - ...this.variables, - ...fetchMoreOptions.variables, - }, - } - )), + ...compact(this.options, { + query, + variables, + context, + errorPolicy, + fetchPolicy, + }), // The fetchMore request goes immediately to the network and does // not automatically write its result to the cache (hence no-cache // instead of network-only), because we allow the caller of @@ -748,13 +749,12 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, // as well as the document passed in `fetchMoreOptions` to ensure the cache // uses the most up-to-date document which may rely on runtime conditionals. this.lastQuery = - fetchMoreOptions.query ? + query ? this.transformDocument(this.options.query) : combinedOptions.query; let wasUpdated = false; - const updateQuery = fetchMoreOptions?.updateQuery; const isCached = this.options.fetchPolicy !== "no-cache"; if (!isCached) { @@ -792,7 +792,6 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, // fetchMore cache results back to this ObservableQuery. this.queryManager.cache.batch({ update: (cache) => { - const { updateQuery } = fetchMoreOptions; if (updateQuery) { cache.updateQuery( { diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts index ba30738aa13..f46bb7be109 100644 --- a/src/core/watchQueryOptions.ts +++ b/src/core/watchQueryOptions.ts @@ -21,6 +21,7 @@ import type { OnQueryUpdated, OperationVariables, } from "./types.js"; +import { Watch } from "typescript"; /** * fetchPolicy determines where the client may return a result from. The options are: @@ -135,13 +136,22 @@ export interface NextFetchPolicyContext< initialFetchPolicy: WatchQueryFetchPolicy; } -export interface FetchMoreQueryOptions { +export type FetchMoreQueryOptions< + TVariables extends OperationVariables = OperationVariables, + TData = unknown, +> = { /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#query:member} */ - query?: DocumentNode | TypedDocumentNode; - /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ - variables?: Partial>; + query: DocumentNode | TypedDocumentNode; + + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#errorPolicy:member} */ + errorPolicy?: ErrorPolicy; + + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#context:member} */ context?: DefaultContext; -} + + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#fetchPolicy:member} */ + fetchPolicy?: FetchPolicy; +} & VariablesOption>; export type UpdateQueryOptions = { variables?: TVariables; From b65982b34d0fd052026a3760b815d6bdeb10ed03 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 18 Jun 2025 18:49:59 +0200 Subject: [PATCH 02/10] adjust options --- src/core/ObservableQuery.ts | 24 ++++++++++++++++-------- src/core/watchQueryOptions.ts | 3 --- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index fedc16f6657..a3e2f1d15cf 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -713,7 +713,6 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, variables, context, errorPolicy, - fetchPolicy, updateQuery, }: FetchMoreQueryOptions & { updateQuery?: ( @@ -725,13 +724,22 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, ) => Unmasked; }): Promise> { const combinedOptions = { - ...compact(this.options, { - query, - variables, - context, - errorPolicy, - fetchPolicy, - }), + ...compact( + this.options, + { errorPolicy: "none" }, + { + query, + context, + errorPolicy, + } + ), + variables: + query ? variables : ( + { + ...this.variables, + ...variables, + } + ), // The fetchMore request goes immediately to the network and does // not automatically write its result to the cache (hence no-cache // instead of network-only), because we allow the caller of diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts index f46bb7be109..30641b70801 100644 --- a/src/core/watchQueryOptions.ts +++ b/src/core/watchQueryOptions.ts @@ -148,9 +148,6 @@ export type FetchMoreQueryOptions< /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#context:member} */ context?: DefaultContext; - - /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#fetchPolicy:member} */ - fetchPolicy?: FetchPolicy; } & VariablesOption>; export type UpdateQueryOptions = { From 71d8aebd7bc960bc6a3f425783b989fd0cda080f Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 18 Jun 2025 18:57:20 +0200 Subject: [PATCH 03/10] undo some type changes --- src/core/ObservableQuery.ts | 4 ++-- src/core/watchQueryOptions.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index a3e2f1d15cf..7cabed6cd9d 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -718,8 +718,8 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, updateQuery?: ( previousQueryResult: Unmasked, options: { - fetchMoreResult: Unmasked; - variables: NoInfer; + fetchMoreResult: Unmasked; + variables: TFetchVars; } ) => Unmasked; }): Promise> { diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts index 30641b70801..3b6e8e2b876 100644 --- a/src/core/watchQueryOptions.ts +++ b/src/core/watchQueryOptions.ts @@ -21,7 +21,6 @@ import type { OnQueryUpdated, OperationVariables, } from "./types.js"; -import { Watch } from "typescript"; /** * fetchPolicy determines where the client may return a result from. The options are: From eae522e4211765d5957b1d70045bfaf99dc38385 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 18 Jun 2025 19:16:56 +0200 Subject: [PATCH 04/10] fix up types --- src/core/ObservableQuery.ts | 30 ++++++++++------------ src/core/watchQueryOptions.ts | 10 ++++---- src/react/hooks/useLoadableQuery.ts | 5 +--- src/react/internal/cache/QueryReference.ts | 7 ++--- src/react/internal/types.ts | 13 ++-------- 5 files changed, 23 insertions(+), 42 deletions(-) diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index 7cabed6cd9d..0c01eca8152 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -49,18 +49,20 @@ import type { const { assign, hasOwnProperty } = Object; -export interface FetchMoreOptions< +export type FetchMoreOptions< TData = unknown, - TVariables = OperationVariables, -> { + TVariables extends OperationVariables = OperationVariables, + TFetchData = TData, + TFetchVars extends OperationVariables = TVariables, +> = FetchMoreQueryOptions & { updateQuery?: ( - previousQueryResult: TData, + previousQueryResult: Unmasked, options: { - fetchMoreResult?: TData; - variables?: TVariables; + fetchMoreResult: Unmasked; + variables: TFetchVars; } - ) => TData; -} + ) => Unmasked; +}; interface TrackedOperation { /** @@ -714,15 +716,9 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, context, errorPolicy, updateQuery, - }: FetchMoreQueryOptions & { - updateQuery?: ( - previousQueryResult: Unmasked, - options: { - fetchMoreResult: Unmasked; - variables: TFetchVars; - } - ) => Unmasked; - }): Promise> { + }: FetchMoreOptions): Promise< + QueryResult + > { const combinedOptions = { ...compact( this.options, diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts index 3b6e8e2b876..5633cf4740e 100644 --- a/src/core/watchQueryOptions.ts +++ b/src/core/watchQueryOptions.ts @@ -14,10 +14,10 @@ import type { IgnoreModifier } from "../cache/core/types/common.js"; import type { ObservableQuery } from "./ObservableQuery.js"; import type { DefaultContext, - NormalizedExecutionResult, InternalRefetchQueriesInclude, MutationQueryReducersMap, MutationUpdaterFunction, + NormalizedExecutionResult, OnQueryUpdated, OperationVariables, } from "./types.js"; @@ -140,14 +140,14 @@ export type FetchMoreQueryOptions< TData = unknown, > = { /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#query:member} */ - query: DocumentNode | TypedDocumentNode; - + query?: DocumentNode | TypedDocumentNode; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ + variables?: Partial>; /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#errorPolicy:member} */ errorPolicy?: ErrorPolicy; - /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#context:member} */ context?: DefaultContext; -} & VariablesOption>; +}; export type UpdateQueryOptions = { variables?: TVariables; diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index dd8e17c2283..5f1e6affbcb 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -6,7 +6,6 @@ import type { DefaultContext, DocumentNode, ErrorPolicy, - FetchMoreQueryOptions, OperationVariables, RefetchWritePolicy, TypedDocumentNode, @@ -234,9 +233,7 @@ export function useLoadableQuery< ); } - const promise = internalQueryRef.fetchMore( - options as FetchMoreQueryOptions - ); + const promise = internalQueryRef.fetchMore(options); setQueryRef(wrapQueryRef(internalQueryRef)); diff --git a/src/react/internal/cache/QueryReference.ts b/src/react/internal/cache/QueryReference.ts index 0489e6a5af3..df456d6601c 100644 --- a/src/react/internal/cache/QueryReference.ts +++ b/src/react/internal/cache/QueryReference.ts @@ -5,6 +5,7 @@ import { filter } from "rxjs"; import type { ApolloQueryResult, DataState, + FetchMoreOptions, ObservableQuery, OperationVariables, QueryResult, @@ -30,10 +31,6 @@ type Listener["dataState"]> = ( promise: QueryRefPromise ) => void; -type FetchMoreOptions = Parameters< - ObservableQuery["fetchMore"] ->[0]; - const QUERY_REFERENCE_SYMBOL: unique symbol = Symbol.for( "apollo.internal.queryRef" ); @@ -350,7 +347,7 @@ export class InternalQueryReference< return this.initiateFetch(this.observable.refetch(variables)); } - fetchMore(options: FetchMoreOptions) { + fetchMore(options: FetchMoreOptions) { return this.initiateFetch(this.observable.fetchMore(options)); } diff --git a/src/react/internal/types.ts b/src/react/internal/types.ts index f7cf0485c85..05fb489c77f 100644 --- a/src/react/internal/types.ts +++ b/src/react/internal/types.ts @@ -1,9 +1,8 @@ import type { - FetchMoreQueryOptions, + FetchMoreOptions, MaybeMasked, OperationVariables, QueryResult, - Unmasked, } from "@apollo/client"; export type RefetchFunction = ( @@ -11,13 +10,5 @@ export type RefetchFunction = ( ) => Promise>; export type FetchMoreFunction = ( - fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: ( - previousQueryResult: Unmasked, - options: { - fetchMoreResult: Unmasked; - variables: TVariables; - } - ) => Unmasked; - } + fetchMoreOptions: FetchMoreOptions ) => Promise>>; From db847ec19cf243f51d721732c1e646bdd088b1e4 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 18 Jun 2025 19:44:06 +0200 Subject: [PATCH 05/10] changesets --- .changeset/mighty-carrots-bathe.md | 7 +++++++ .changeset/old-melons-double.md | 11 +++++++++++ 2 files changed, 18 insertions(+) create mode 100644 .changeset/mighty-carrots-bathe.md create mode 100644 .changeset/old-melons-double.md diff --git a/.changeset/mighty-carrots-bathe.md b/.changeset/mighty-carrots-bathe.md new file mode 100644 index 00000000000..8fb9e1e13b6 --- /dev/null +++ b/.changeset/mighty-carrots-bathe.md @@ -0,0 +1,7 @@ +--- +"@apollo/client": minor +--- + +Allow passing `errorPolicy` option to `fetchMore` and change default value. + +Also, the `FetchMoreOptions` type now also includes the `FetchMoreQueryOptions` type. diff --git a/.changeset/old-melons-double.md b/.changeset/old-melons-double.md new file mode 100644 index 00000000000..8fbe2b62987 --- /dev/null +++ b/.changeset/old-melons-double.md @@ -0,0 +1,11 @@ +--- +"@apollo/client": major +--- + +Rework option handling for `fetchMore`. + +* Previously, if the `query` option was specified, no options would be inherited +from the underlying `ObservableQuery`. +Now, even if `query` is specified, all unspecified options except for `variables` will be inherited from the underlying `ObservableQuery`. +* If `query` is not specified, `variables` will still be shallowly merged with the `variables` of the underlying `ObservableQuery` (this also was the previous behaviour). +* `errorPolicy` of `fetchMore` will now always default to `"none"` instead of being inherited. From 33b41ed905d3a4b062c1db877d446a3301ba59c3 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 18 Jun 2025 21:09:15 +0200 Subject: [PATCH 06/10] type changes --- .changeset/mighty-carrots-bathe.md | 3 +-- .changeset/sixty-bears-bathe.md | 6 +++++ src/core/ObservableQuery.ts | 31 ++++++++++++++++---------- src/core/index.ts | 3 +-- src/core/watchQueryOptions.ts | 14 ------------ src/react/hooks/useBackgroundQuery.ts | 3 +-- src/react/hooks/useLazyQuery.ts | 18 ++++++--------- src/react/hooks/useQuery.ts | 19 +++++++--------- src/react/hooks/useQueryRefHandlers.ts | 9 +++++--- src/react/internal/types.ts | 7 ++++-- 10 files changed, 54 insertions(+), 59 deletions(-) create mode 100644 .changeset/sixty-bears-bathe.md diff --git a/.changeset/mighty-carrots-bathe.md b/.changeset/mighty-carrots-bathe.md index 8fb9e1e13b6..1a011f94bc4 100644 --- a/.changeset/mighty-carrots-bathe.md +++ b/.changeset/mighty-carrots-bathe.md @@ -2,6 +2,5 @@ "@apollo/client": minor --- -Allow passing `errorPolicy` option to `fetchMore` and change default value. +Allow passing `errorPolicy` option to `fetchMore` and change default value to "none". -Also, the `FetchMoreOptions` type now also includes the `FetchMoreQueryOptions` type. diff --git a/.changeset/sixty-bears-bathe.md b/.changeset/sixty-bears-bathe.md new file mode 100644 index 00000000000..183099382a3 --- /dev/null +++ b/.changeset/sixty-bears-bathe.md @@ -0,0 +1,6 @@ +--- +"@apollo/client": minor +--- + +The `FetchMoreQueryOptions` type has been inlined into `FetchMoreOptions`, and +`FetchMoreQueryOptions` has been removed. diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index 0c01eca8152..279985fa2e0 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -37,8 +37,8 @@ import type { } from "./types.js"; import type { ErrorPolicy, - FetchMoreQueryOptions, NextFetchPolicyContext, + QueryOptions, RefetchWritePolicy, SubscribeToMoreOptions, UpdateQueryMapFn, @@ -50,11 +50,19 @@ import type { const { assign, hasOwnProperty } = Object; export type FetchMoreOptions< - TData = unknown, - TVariables extends OperationVariables = OperationVariables, + TData, + TVariables extends OperationVariables, TFetchData = TData, TFetchVars extends OperationVariables = TVariables, -> = FetchMoreQueryOptions & { +> = { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#query:member} */ + query?: DocumentNode | TypedDocumentNode; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ + variables?: Partial>; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#errorPolicy:member} */ + errorPolicy?: ErrorPolicy; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#context:member} */ + context?: DefaultContext; updateQuery?: ( previousQueryResult: Unmasked, options: { @@ -729,13 +737,12 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, errorPolicy, } ), - variables: - query ? variables : ( - { - ...this.variables, - ...variables, - } - ), + variables: (query ? variables : ( + { + ...this.variables, + ...variables, + } + )) as TFetchVars, // The fetchMore request goes immediately to the network and does // not automatically write its result to the cache (hence no-cache // instead of network-only), because we allow the caller of @@ -743,7 +750,7 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, // the data gets written to the cache. fetchPolicy: "no-cache", notifyOnNetworkStatusChange: this.options.notifyOnNetworkStatusChange, - } as WatchQueryOptions; + } as QueryOptions; combinedOptions.query = this.transformDocument(combinedOptions.query); diff --git a/src/core/index.ts b/src/core/index.ts index 11ac17daf18..7ac63579216 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -6,7 +6,6 @@ export type { FetchMoreOptions } from "./ObservableQuery.js"; export { ObservableQuery } from "./ObservableQuery.js"; export type { ErrorPolicy, - FetchMoreQueryOptions, FetchPolicy, MutationFetchPolicy, MutationOptions, @@ -27,7 +26,6 @@ export type { DataState, DefaultContext, ErrorLike, - NormalizedExecutionResult, GetDataState, InternalRefetchQueriesInclude, InternalRefetchQueriesMap, @@ -39,6 +37,7 @@ export type { MutationQueryReducer, MutationQueryReducersMap, MutationUpdaterFunction, + NormalizedExecutionResult, OnQueryUpdated, OperationVariables, QueryResult, diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts index 5633cf4740e..a2a7898ec06 100644 --- a/src/core/watchQueryOptions.ts +++ b/src/core/watchQueryOptions.ts @@ -135,20 +135,6 @@ export interface NextFetchPolicyContext< initialFetchPolicy: WatchQueryFetchPolicy; } -export type FetchMoreQueryOptions< - TVariables extends OperationVariables = OperationVariables, - TData = unknown, -> = { - /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#query:member} */ - query?: DocumentNode | TypedDocumentNode; - /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ - variables?: Partial>; - /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#errorPolicy:member} */ - errorPolicy?: ErrorPolicy; - /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#context:member} */ - context?: DefaultContext; -}; - export type UpdateQueryOptions = { variables?: TVariables; } & ( diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index 9f7f466205b..8e0f6ab44c5 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -6,7 +6,6 @@ import type { DefaultContext, DocumentNode, ErrorPolicy, - FetchMoreQueryOptions, OperationVariables, RefetchWritePolicy, TypedDocumentNode, @@ -382,7 +381,7 @@ function useBackgroundQuery_< const fetchMore: FetchMoreFunction = React.useCallback( (options) => { - const promise = queryRef.fetchMore(options as FetchMoreQueryOptions); + const promise = queryRef.fetchMore(options); setWrappedQueryRef(wrapQueryRef(queryRef)); diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index bb8dcb3b9b1..cf0e6a2b8ba 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -10,7 +10,7 @@ import type { DefaultContext, ErrorLike, ErrorPolicy, - FetchMoreQueryOptions, + FetchMoreOptions, GetDataState, MaybeMasked, ObservableQuery, @@ -18,7 +18,6 @@ import type { QueryResult, RefetchWritePolicy, SubscribeToMoreFunction, - Unmasked, UpdateQueryMapFn, WatchQueryFetchPolicy, WatchQueryOptions, @@ -106,15 +105,12 @@ export declare namespace useLazyQuery { TFetchData = TData, TFetchVars extends OperationVariables = TVariables, >( - fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: ( - previousQueryResult: Unmasked, - options: { - fetchMoreResult: Unmasked; - variables: TFetchVars; - } - ) => Unmasked; - } + fetchMoreOptions: FetchMoreOptions< + TData, + TVariables, + TFetchData, + TFetchVars + > ) => Promise>>; /** {@inheritDoc @apollo/client!QueryResultDocumentation#client:member} */ diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 4cc003c6752..a43fc7977c3 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -23,7 +23,7 @@ import type { DocumentNode, ErrorLike, ErrorPolicy, - FetchMoreQueryOptions, + FetchMoreOptions, GetDataState, ObservableQuery, OperationVariables, @@ -36,7 +36,7 @@ import type { WatchQueryOptions, } from "@apollo/client"; import { NetworkStatus } from "@apollo/client"; -import type { MaybeMasked, Unmasked } from "@apollo/client/masking"; +import type { MaybeMasked } from "@apollo/client/masking"; import type { NoInfer, VariablesOption, @@ -152,15 +152,12 @@ export declare namespace useQuery { TFetchData = TData, TFetchVars extends OperationVariables = TVariables, >( - fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: ( - previousQueryResult: Unmasked, - options: { - fetchMoreResult: Unmasked; - variables: TFetchVars; - } - ) => Unmasked; - } + fetchMoreOptions: FetchMoreOptions< + TData, + TVariables, + TFetchData, + TFetchVars + > ) => Promise>>; } & GetDataState, TStates>; } diff --git a/src/react/hooks/useQueryRefHandlers.ts b/src/react/hooks/useQueryRefHandlers.ts index 327ad3d2d6f..39a701c6bae 100644 --- a/src/react/hooks/useQueryRefHandlers.ts +++ b/src/react/hooks/useQueryRefHandlers.ts @@ -1,8 +1,11 @@ import * as React from "react"; -import type { DataState, OperationVariables } from "@apollo/client"; +import type { + DataState, + FetchMoreOptions, + OperationVariables, +} from "@apollo/client"; import type { SubscribeToMoreFunction } from "@apollo/client"; -import type { FetchMoreQueryOptions } from "@apollo/client"; import type { ApolloClient } from "@apollo/client"; import type { ObservableQuery } from "@apollo/client"; import type { @@ -113,7 +116,7 @@ function useQueryRefHandlers_< const fetchMore: FetchMoreFunction = React.useCallback( (options) => { const promise = internalQueryRef.fetchMore( - options as FetchMoreQueryOptions + options as FetchMoreOptions ); setWrappedQueryRef(wrapQueryRef(internalQueryRef)); diff --git a/src/react/internal/types.ts b/src/react/internal/types.ts index 05fb489c77f..603e3dfd91c 100644 --- a/src/react/internal/types.ts +++ b/src/react/internal/types.ts @@ -9,6 +9,9 @@ export type RefetchFunction = ( variables?: Partial ) => Promise>; -export type FetchMoreFunction = ( - fetchMoreOptions: FetchMoreOptions +export type FetchMoreFunction = < + TFetchData = TData, + TFetchVars extends OperationVariables = TVariables, +>( + fetchMoreOptions: FetchMoreOptions ) => Promise>>; From 7c6f7ee96c517ced66888a7c4fbb6a14455c9750 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 18 Jun 2025 21:15:04 +0200 Subject: [PATCH 07/10] changeset changes --- .changeset/old-melons-double.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/old-melons-double.md b/.changeset/old-melons-double.md index 8fbe2b62987..bde0eda5a7e 100644 --- a/.changeset/old-melons-double.md +++ b/.changeset/old-melons-double.md @@ -7,5 +7,5 @@ Rework option handling for `fetchMore`. * Previously, if the `query` option was specified, no options would be inherited from the underlying `ObservableQuery`. Now, even if `query` is specified, all unspecified options except for `variables` will be inherited from the underlying `ObservableQuery`. -* If `query` is not specified, `variables` will still be shallowly merged with the `variables` of the underlying `ObservableQuery` (this also was the previous behaviour). -* `errorPolicy` of `fetchMore` will now always default to `"none"` instead of being inherited. +* If `query` is not specified, `variables` will still be shallowly merged with the `variables` of the underlying `ObservableQuery`. If a `query` option is specified, the `variables` passed to `fetchMore` are used instead. +* `errorPolicy` of `fetchMore` will now always default to `"none"` instead of inherited from the `ObservableQuery` options. This can prevent accidental cache writes of partial data for a paginated query. To opt into receive partial data that may be written to the cache, pass an `errorPolicy` to `fetchMore` to override the default. From 5abbe5b52d60418556a3f8826e8fd4861124d38b Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 18 Jun 2025 21:17:26 +0200 Subject: [PATCH 08/10] fixup & chores --- .api-reports/api-report-core.api.md | 36 ++++------- .api-reports/api-report-react.api.md | 63 ++++++++----------- .api-reports/api-report-react_internal.api.md | 17 +---- .api-reports/api-report.api.md | 38 +++++------ .size-limits.json | 8 +-- src/react/internal/cache/QueryReference.ts | 2 +- 6 files changed, 61 insertions(+), 103 deletions(-) diff --git a/.api-reports/api-report-core.api.md b/.api-reports/api-report-core.api.md index bd7c543b21f..bb88e62c225 100644 --- a/.api-reports/api-report-core.api.md +++ b/.api-reports/api-report-core.api.md @@ -305,21 +305,16 @@ export { execute } export { fallbackHttpConfig } // @public (undocumented) -export interface FetchMoreOptions { - // (undocumented) - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult?: TData; - variables?: TVariables; - }) => TData; -} - -// @public (undocumented) -export interface FetchMoreQueryOptions { - // (undocumented) +export type FetchMoreOptions = { + query?: DocumentNode_2 | TypedDocumentNode; + variables?: Partial>; + errorPolicy?: ErrorPolicy; context?: DefaultContext; - query?: DocumentNode_2 | TypedDocumentNode; - variables?: Partial>; -} + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; + variables: TFetchVars; + }) => Unmasked; +}; // @public export type FetchPolicy = "cache-first" | "network-only" | "cache-only" | "no-cache"; @@ -586,12 +581,7 @@ export class ObservableQuery; queryId?: string; }); - fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: Unmasked, options: { - fetchMoreResult: Unmasked; - variables: TFetchVars; - }) => Unmasked; - }): Promise>; + fetchMore({ query, variables, context, errorPolicy, updateQuery, }: FetchMoreOptions): Promise>; // @internal @deprecated (undocumented) getCacheDiff({ optimistic }?: { optimistic?: boolean | undefined; @@ -1041,10 +1031,10 @@ export type WatchQueryOptions { - // (undocumented) +type FetchMoreOptions = { + query?: DocumentNode | TypedDocumentNode; + variables?: Partial>; + errorPolicy?: ErrorPolicy; context?: DefaultContext; - query?: DocumentNode | TypedDocumentNode; - variables?: Partial>; -} + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; + variables: TFetchVars; + }) => Unmasked; +}; // @public type FetchPolicy = "cache-first" | "network-only" | "cache-only" | "no-cache"; @@ -560,13 +564,8 @@ class ObservableQuery; queryId?: string; }); - // Warning: (ae-forgotten-export) The symbol "FetchMoreQueryOptions" needs to be exported by the entry point index.d.ts - fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: Unmasked, options: { - fetchMoreResult: Unmasked; - variables: TFetchVars; - }) => Unmasked; - }): Promise>; + // Warning: (ae-forgotten-export) The symbol "FetchMoreOptions" needs to be exported by the entry point index.d.ts + fetchMore({ query, variables, context, errorPolicy, updateQuery, }: FetchMoreOptions): Promise>; // @internal @deprecated (undocumented) getCacheDiff({ optimistic }?: { optimistic?: boolean | undefined; @@ -1225,12 +1224,7 @@ export namespace useLazyQuery { subscribeToMore: SubscribeToMoreFunction; updateQuery: (mapFn: UpdateQueryMapFn_2) => void; refetch: (variables?: Partial) => Promise>>; - fetchMore: (fetchMoreOptions: FetchMoreQueryOptions_2 & { - updateQuery?: (previousQueryResult: Unmasked_2, options: { - fetchMoreResult: Unmasked_2; - variables: TFetchVars; - }) => Unmasked_2; - }) => Promise>>; + fetchMore: (fetchMoreOptions: FetchMoreOptions_2) => Promise>>; client: ApolloClient; observable: ObservableQuery_2; previousData?: MaybeMasked_2; @@ -1410,12 +1404,7 @@ export namespace useQuery { updateQuery: (mapFn: UpdateQueryMapFn_2) => void; refetch: (variables?: Partial) => Promise>>; variables: TVariables; - fetchMore: (fetchMoreOptions: FetchMoreQueryOptions_2 & { - updateQuery?: (previousQueryResult: Unmasked, options: { - fetchMoreResult: Unmasked; - variables: TFetchVars; - }) => Unmasked; - }) => Promise>>; + fetchMore: (fetchMoreOptions: FetchMoreOptions_2) => Promise>>; } & GetDataState_2, TStates>; } @@ -1640,23 +1629,23 @@ type WatchQueryOptions_2 = (fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: Unmasked, options: { - fetchMoreResult: Unmasked; - variables: TVariables; - }) => Unmasked; -}) => Promise>>; - -// @public (undocumented) -type FetchMoreOptions = Parameters["fetchMore"]>[0]; +export type FetchMoreFunction = (fetchMoreOptions: FetchMoreOptions) => Promise>>; // @public (undocumented) type FragmentCacheKey = [ @@ -138,10 +129,8 @@ export class InternalQueryReference): Promise>; + fetchMore(options: FetchMoreOptions): Promise>; // (undocumented) readonly key: QueryKey; // Warning: (ae-forgotten-export) The symbol "Listener" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index efaa0f39fbe..6b10dbd84b4 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -841,22 +841,16 @@ export const fallbackHttpConfig: { }; // @public (undocumented) -export interface FetchMoreOptions { - // (undocumented) - updateQuery?: (previousQueryResult: TData, options: { - fetchMoreResult?: TData; - variables?: TVariables; - }) => TData; -} - -// @public (undocumented) -export interface FetchMoreQueryOptions { - // (undocumented) +export type FetchMoreOptions = { + query?: DocumentNode | TypedDocumentNode; + variables?: Partial>; + errorPolicy?: ErrorPolicy; context?: DefaultContext; - query?: DocumentNode | TypedDocumentNode; - // Warning: (ae-forgotten-export) The symbol "NoInfer_2" needs to be exported by the entry point index.d.ts - variables?: Partial>; -} + updateQuery?: (previousQueryResult: Unmasked, options: { + fetchMoreResult: Unmasked; + variables: TFetchVars; + }) => Unmasked; +}; // @public export type FetchPolicy = "cache-first" | "network-only" | "cache-only" | "no-cache"; @@ -1521,6 +1515,7 @@ namespace LocalState { // // @public (undocumented) class LocalState, TContext = InferContextValueFromResolvers> { + // Warning: (ae-forgotten-export) The symbol "NoInfer_2" needs to be exported by the entry point index.d.ts constructor(...[options]: {} extends TResolvers ? [ options?: LocalState.Options> ] : [ @@ -1905,12 +1900,7 @@ export class ObservableQuery; queryId?: string; }); - fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { - updateQuery?: (previousQueryResult: Unmasked, options: { - fetchMoreResult: Unmasked; - variables: TFetchVars; - }) => Unmasked; - }): Promise>; + fetchMore({ query, variables, context, errorPolicy, updateQuery, }: FetchMoreOptions): Promise>; // @internal @deprecated (undocumented) getCacheDiff({ optimistic }?: { optimistic?: boolean | undefined; @@ -2700,10 +2690,10 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/policies.ts:166:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:166:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts // src/cache/inmemory/types.ts:133:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:133:5 - (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:293:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:143:5 - (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:303:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:187:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:261:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:253:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts // src/local-state/LocalState.ts:140:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts // src/local-state/LocalState.ts:174:7 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts // src/local-state/LocalState.ts:194:7 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts diff --git a/.size-limits.json b/.size-limits.json index e8fa89f8541..7c5de935ae0 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,6 +1,6 @@ { - "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 43543, - "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 38572, - "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33302, - "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27599 + "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 43591, + "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 38584, + "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33313, + "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27573 } diff --git a/src/react/internal/cache/QueryReference.ts b/src/react/internal/cache/QueryReference.ts index df456d6601c..d3477c33cd8 100644 --- a/src/react/internal/cache/QueryReference.ts +++ b/src/react/internal/cache/QueryReference.ts @@ -347,7 +347,7 @@ export class InternalQueryReference< return this.initiateFetch(this.observable.refetch(variables)); } - fetchMore(options: FetchMoreOptions) { + fetchMore(options: FetchMoreOptions) { return this.initiateFetch(this.observable.fetchMore(options)); } From 498508d9ce2786f9c6ab510914e5d078cbff57cf Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 18 Jun 2025 21:37:47 +0200 Subject: [PATCH 09/10] add tests --- src/__tests__/fetchMore.ts | 193 ++++++++++++++++++++++++++++++++++++- 1 file changed, 192 insertions(+), 1 deletion(-) diff --git a/src/__tests__/fetchMore.ts b/src/__tests__/fetchMore.ts index f625283dac8..af513f5129d 100644 --- a/src/__tests__/fetchMore.ts +++ b/src/__tests__/fetchMore.ts @@ -223,7 +223,7 @@ describe("fetchMore on an observable query", () => { limit: 20, }; - const result: any = { + const result: { data: TCommentData } = { data: { entry: { __typename: "Entry", @@ -1667,6 +1667,197 @@ describe("fetchMore on an observable query", () => { await expect(stream).not.toEmitAnything(); }); + + test("`errorPolicy` defaults to `none`", async () => { + const query = gql` + query { + fail + } + `; + const observable = setup({ + request: { query }, + error: new Error("This is an error"), + }); + const stream = new ObservableStream(observable); + await expect(stream).toEmitTypedValue({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + await expect(stream).toEmitTypedValue({ + data: result.data, + dataState: "complete", + loading: false, + networkStatus: NetworkStatus.ready, + partial: false, + }); + + const updateQuery = jest.fn(); + const promise = observable.fetchMore({ + query, + updateQuery, + }); + await expect(promise).rejects.toThrow("This is an error"); + expect(updateQuery).not.toHaveBeenCalled(); + await expect(stream).toEmitSimilarValue({ + expected: (previous) => ({ + ...previous, + loading: true, + networkStatus: NetworkStatus.fetchMore, + }), + }); + await expect(stream).toEmitSimilarValue({ + expected: (previous) => ({ + ...previous, + loading: false, + networkStatus: NetworkStatus.ready, + }), + }); + await expect(stream).not.toEmitAnything(); + }); + + test("`errorPolicy` can be overwritten to `ignore`", async () => { + const query = gql` + query { + fail + } + `; + const observable = setup({ + request: { query }, + error: new Error("This is an error"), + }); + const stream = new ObservableStream(observable); + await expect(stream).toEmitTypedValue({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + await expect(stream).toEmitTypedValue({ + data: result.data, + dataState: "complete", + loading: false, + networkStatus: NetworkStatus.ready, + partial: false, + }); + + const fallbackResult: TCommentData = { + entry: { + ...result.data.entry, + comments: result.data.entry.comments.concat({ + __typename: "Comment", + text: "fallback comment", + }), + }, + }; + + const updateQuery = jest.fn( + (previousResult: TCommentData): TCommentData => fallbackResult + ); + const promise = observable.fetchMore({ + query, + updateQuery, + errorPolicy: "ignore", + }); + await expect(promise).resolves.toStrictEqualTyped({ data: undefined }); + expect(updateQuery).toHaveBeenCalledTimes(1); + expect(updateQuery).toHaveBeenNthCalledWith(1, result.data, { + fetchMoreResult: undefined, + variables: undefined, + }); + await expect(stream).toEmitSimilarValue({ + expected: (previous) => ({ + ...previous, + loading: true, + networkStatus: NetworkStatus.fetchMore, + }), + }); + await expect(stream).toEmitSimilarValue({ + expected: (previous) => ({ + ...previous, + data: fallbackResult, + dataState: "complete", + loading: false, + networkStatus: NetworkStatus.ready, + }), + }); + await expect(stream).not.toEmitAnything(); + }); + + test("`errorPolicy` can be overwritten to `all`", async () => { + const query = gql` + query { + fail + } + `; + const observable = setup({ + request: { query }, + error: new Error("This is an error"), + }); + const stream = new ObservableStream(observable); + await expect(stream).toEmitTypedValue({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + await expect(stream).toEmitTypedValue({ + data: result.data, + dataState: "complete", + loading: false, + networkStatus: NetworkStatus.ready, + partial: false, + }); + + const fallbackResult: TCommentData = { + entry: { + ...result.data.entry, + comments: result.data.entry.comments.concat({ + __typename: "Comment", + text: "fallback comment", + }), + }, + }; + + const updateQuery = jest.fn( + (previousResult: TCommentData): TCommentData => fallbackResult + ); + const promise = observable.fetchMore({ + query, + updateQuery, + errorPolicy: "all", + }); + await expect(promise).resolves.toStrictEqual({ + data: undefined, + error: new Error("This is an error"), + }); + expect(updateQuery).toHaveBeenCalledTimes(1); + expect(updateQuery).toHaveBeenNthCalledWith(1, result.data, { + fetchMoreResult: undefined, + variables: undefined, + }); + await expect(stream).toEmitSimilarValue({ + expected: (previous) => ({ + ...previous, + loading: true, + networkStatus: NetworkStatus.fetchMore, + }), + }); + await expect(stream).toEmitSimilarValue({ + expected: (previous) => ({ + ...previous, + data: fallbackResult, + dataState: "complete", + loading: false, + networkStatus: NetworkStatus.ready, + }), + }); + await expect(stream).not.toEmitAnything(); + }); }); describe("fetchMore on an observable query with connection", () => { From 6e67033c352486abc9f443a51d4b2cd16130e23b Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 18 Jun 2025 22:47:18 +0200 Subject: [PATCH 10/10] update test --- src/__tests__/fetchMore.ts | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/__tests__/fetchMore.ts b/src/__tests__/fetchMore.ts index af513f5129d..7f3e1f328da 100644 --- a/src/__tests__/fetchMore.ts +++ b/src/__tests__/fetchMore.ts @@ -3,7 +3,13 @@ import { assign, cloneDeep } from "lodash"; import { Observable } from "rxjs"; import type { TypedDocumentNode } from "@apollo/client"; -import { ApolloClient, ApolloLink, NetworkStatus, split } from "@apollo/client"; +import { + ApolloClient, + ApolloLink, + CombinedGraphQLErrors, + NetworkStatus, + split, +} from "@apollo/client"; import type { ApolloCache, FieldMergeFunction, @@ -1795,7 +1801,14 @@ describe("fetchMore on an observable query", () => { `; const observable = setup({ request: { query }, - error: new Error("This is an error"), + result: { + data: { + entry: { + __typename: "Entry", + }, + }, + errors: [{ message: "This is an error" }], + }, }); const stream = new ObservableStream(observable); await expect(stream).toEmitTypedValue({ @@ -1832,12 +1845,27 @@ describe("fetchMore on an observable query", () => { errorPolicy: "all", }); await expect(promise).resolves.toStrictEqual({ - data: undefined, - error: new Error("This is an error"), + data: { + entry: { + __typename: "Entry", + }, + }, + error: new CombinedGraphQLErrors({ + data: { + entry: { + __typename: "Entry", + }, + }, + errors: [{ message: "This is an error" }], + }), }); expect(updateQuery).toHaveBeenCalledTimes(1); expect(updateQuery).toHaveBeenNthCalledWith(1, result.data, { - fetchMoreResult: undefined, + fetchMoreResult: { + entry: { + __typename: "Entry", + }, + }, variables: undefined, }); await expect(stream).toEmitSimilarValue({