A fully-featured caching GraphQL client.
Last release 6 days ago
21 Aug 2026
Ships on a steady schedule
a new release about every 2 weeks
Nearly every release is documented
notes for 60 of the last 60 stable releases
27 versions withdrawn
withdrawn after publishing
7 years old
723 releases · first in 2019
Release timeline
723 releases since 2019Releases
- 4.3.0-rc.021 Aug 2026pre-release
Release notes
Open source →Minor Changes
- #13426
a9beaffThanks @jerelmiller! - Version bump only torc.
- #13426
- 4.3.0-alpha.1121 Aug 2026pre-release
Release notes
Open source →Minor Changes
-
#13386
0be8fd8Thanks @atharv-sys32! - SupportskipTokenwithuseSubscriptionto provide a more type-safe way to skip subscription execution with required variables.import { skipToken, useSubscription } from "@apollo/client/react"; // Use `skipToken` in place of `skip: true` for better type safety // for required variables const { data } = useSubscription( SUBSCRIPTION, id ? { variables: { id } } : skipToken );
-
#13424
d2bca2eThanks @jerelmiller! - Remove the customNoInfertype utility in favor of the nativeNoInferintroduced in TypeScript 5.4.
-
- 4.3.0-alpha.1019 Aug 2026pre-release
Release notes
Open source →Minor Changes
-
#13421
d6197a4Thanks @jerelmiller! - The minimum supported TypeScript version is now 5.9.x. -
#13337
2df711fThanks @jcostello-atlassian! - Allow overriding thefrominput ofuseFragment,useSuspenseFragment,readFragment,writeFragmentand related fragment APIs via a newFromOptionValuekey on theTypeOverridesinterface.By default,
fromcontinues to acceptStoreObject | Reference | FragmentType<TData> | string. Apps can now supply a stricter policy (for example, requiring__typenameand disallowing nullish identifier values) without affectingStoreObject,cache.identify,cache.modifyor optimistic writes.// apollo.d.ts import "@apollo/client"; import type { HKT, StoreValue } from "@apollo/client/utilities"; type StrictFrom<TData extends { __typename: string }> = | { // the `__typename` has to match the one of the fragment type __typename: TData["__typename"]; // `& {}` forces values to be "defined" so an explicit `undefined` // (as well as `null`) is rejected. [key: string]: Exclude<StoreValue, null | undefined> & {}; } | { __ref: string } | string | null; interface StrictFromHKT extends HKT { arg1: { __typename: string }; // TData return: StrictFrom<this["arg1"]>; } declare module "@apollo/client" { export interface TypeOverrides { FromOptionValue: StrictFromHKT; } }
-
- 4.3.0-alpha.917 Aug 2026pre-release
Release notes
Open source →Minor Changes
- #13416
f2d5d5aThanks @jerelmiller! - AddGraphQLCodegenIncrementaltype overrides that assemble GraphQL Codegen@deferoperation types whendataStateis"complete".
- #13416
- 4.3.0-alpha.814 Aug 2026pre-release
Release notes
Open source →Minor Changes
- #13372
e4cde69Thanks @jerelmiller! - Parse scalar fields forno-cachequeries.
- #13372
- 4.3.0-alpha.713 Aug 2026pre-release
Release notes
Open source →Minor Changes
-
#13406
bd74ccbThanks @jerelmiller! - Emit a development-only warning when a feud is detected between queries that overwrite each other's data. This should make it easier to detect when you need to select a key field or add amergefunction to a field policy. -
#13406
bd74ccbThanks @jerelmiller! - Fixes an issue where cache feuds between queries selecting incompatible non-normalized data could return untransformed network values.Apollo Client now always writes network results to the cache before delivering them, ensuring custom scalars and field
readfunctions are applied. To prevent repeated refetches when competing queries repeatedly make each other's cache results incomplete, Apollo Client stops automatically refetching a query after it sees the same incomplete result again.This may add one network request in these cache-feud scenarios.
-
- 4.3.0-alpha.613 Aug 2026pre-release
Release notes
Open source →Minor Changes
- #13405
f923ab4Thanks @jerelmiller! - Field policyreadandmergefunctions are now ignored when the field policy configures thescalaroption. If areadormergefunction is provided alongsidescalar, a development-only warning is emitted.
Patch Changes
- #13408
7a5164dThanks @jerelmiller! - FixdataStateto report"streaming"instead of"partial"whenreturnPartialDataistrueand the cache result is missing only@deferfields.
- #13405
- 4.3.0-alpha.511 Aug 2026pre-release
Release notes
Open source →Minor Changes
-
#13390
90e338cThanks @jerelmiller! - Fix issue where sibling@deferfragments were pruned incorrectly when at least one of the@deferfragments wasn't delivered.As a result of this change, a
labelargument is now added to all outgoing@deferdirectives when using theGraphQL17Alpha9Handlerin order to disambiguate the@deferfragments from each other. -
#13393
434d25fThanks @jerelmiller! - Change when@deferfragments and@streamfields are pruned forcache-firstandcache-and-networkfetch policies to better match the network when the initial value contained a partial result:cache-first: prune undelivered@deferfragments or@streamitems when the result is fetched from the network due to a partial resultcache-and-network: prune undelivered@deferfragments or@streamitems if the initial cache value was partial. If the first value emitted from the cache is complete, the results will not be pruned.
This makes the emitted results more predictable by following what the network has delivered and avoids some ambiguity in other edge cases.
For example, with a
cache-firstfetch policy where all@deferfields are written to the cache, but a non-deferred field is partial, the values emitted from the client previously looked like the following:query { user { id name ... @defer { email } } }
// data written to the cache is missing name { user: { id: 1, email: "[email protected]" }} // 1. empty because the result is partial { data: undefined, dataState: "empty", ... } // 2. returns all data because the cache contains a value for email { data: { user: 1, name: "User", email: "[email protected]" }, dataState: "complete" } // 3. email updated from the server { data: { user: 1, name: "User", email: "[email protected]" }, dataState: "complete" }
Here the result is confusing because the initial value returned from the query was
undefined, yet a complete result was returned after the initial chunk from the network returned (which did not containemail).The cache values are now pruned if the network hasn't delivered them yet:
// 1. empty because the result is partial { data: undefined, dataState: "empty" } // 2. email hasn't been delivered by the network so it gets pruned { data: { user: 1, name: "User" }, dataState: "streaming" } // 3. full result returned after the network streams the email field { data: { user: 1, name: "User", email: "[email protected]" }, dataState: "complete" }
This is especially helpful in situations where
@deferboundaries that are never delivered due to errors prevent an awkward situation where the client would otherwise have to choose whether to serve the stale cache result from the cache, or prune the undelivered fragment on the final chunk.
Patch Changes
-
#13381
9c73762Thanks @jerelmiller! - Fix an issue where anetwork-onlyquery leaked partial cache data for@deferfragments that were not delivered by the network due to an error that bubbled to the@deferfragment boundary. -
#13390
90e338cThanks @jerelmiller! - Fix an issue where a sibling non-deferred fragment might be accidentally pruned when the@deferfragment hadn't been delivered. -
#13403
aaff7a8Thanks @jerelmiller! - Fix issue where the wrongdataStatewas returned when there was nothing written to the cache and a@deferfragment was marked pending. -
#13381
9c73762Thanks @jerelmiller! - Fix an issue where a@deferquery reported thedataStateascompleteinstead ofstreamingwhen an error occurs on a deferred field that bubbled to the defer boundary. -
#13373
2551937Thanks @jerelmiller! - Fix an issue where a cache write in the middle of polling would remain as the query value if future poll requests returned deep equal results to previous polling results. -
#13403
aaff7a8Thanks @jerelmiller! - Fix issue where settingreturnPartialData: truemight report the wrongdataStatewhen partial data was written to the cache and@deferfragments were pending. -
#13381
9c73762Thanks @jerelmiller! - Fix an invariant error thrown when a@deferboundary received a payload after it had already been marked complete.
-
- 4.3.0-alpha.424 Jul 2026pre-release
Release notes
Open source →Patch Changes
-
#13347
7d543d6Thanks @jerelmiller! - Fix an issue wherenetwork-onlyincremental queries could cause cache data to leak into the emitted result when a@deferor@streamboundary already had complete data in the cache. Cache data inside pending@deferobjects and@streamarrays are now pruned so that only completed@deferor@streamboundaries are returned.NOTE: This change only applies to
InMemoryCachewhen usingGraphQL17Alpha9Handler. -
#13329
1d581d2Thanks @AmariahAK! - Cache diffs for incomplete queries no longer pay the cost of building a fullMissingFieldErrorwhen themissingproperty is not accessed. The error object is now only constructed when themissingproperty is accessed the first time. This improves performance by avoiding a V8 stack capture whenmissingis ignored entirely.As an additional small performance improvement,
JSON.stringifyis no longer used in the error message on objects whose cache ID is known.JSON.stringifyis only used for non-normalized objects. -
#13347
7d543d6Thanks @jerelmiller! - Fix an issue where partial cache data could leak into intermediate incremental results. This could cause runtime crashes if you relied on the presence of values to determine whether the@deferdata had streamed in or not.
-
- 4.3.0-alpha.313 Jul 2026pre-release
Release notes
Open source →Minor Changes
-
#13324
0abd8deThanks @jerelmiller! - Fix the accuracy ofdataStatein complex incremental streaming scenarios, especially when combined withreturnPartialData: true.Prior to this change, all intermediate chunks used for both
@deferand@streamdirectives returned adataStateofstreaming, regardless of whether the actual data shape fit the definition of thestreamingdata state. Thestreamingdata state represents an incomplete incremental response where the only holes in the data occur at@deferboundaries.Let's use the following example of where the previous
dataStatefell down when combined withreturnPartialData.query GreetingQuery { greeting { message ... @defer { recipient { name email } } } }- Scenario 1: partial data inside a
@deferboundary written to the cache
Let's say the cache contained the following partial data:
{ greeting: { __typename: "Greeting", recipient: { __typename: "Person", name: "John Doe", }, }, };After the first chunk arrives from the server, the data looks like the following:
{ greeting: { __typename: "Greeting", message: "Hello, John", recipient: { __typename: "Person", name: "John Doe", }, }, };This data is not
completebecauserecipient.emailis missing. This data is also notstreamingbecause the data requirements in the@deferboundary are partially fulfilled due to the existence ofrecipient. This could lead to runtime crashes onrecipient.emailif you use the existence ofrecipientto detect whether data in the@deferboundary has streamed in or not. This change now accurately reports this aspartialto ensure the field is marked as a partial field inrecipient.- Scenario 2: partial data written to the cache that fulfills the data requirements of the
@deferboundary
Let's say the cache contained the following partial data:
{ greeting: { __typename: "Greeting", recipient: { __typename: "Person", name: "John Doe", email: "[email protected]", }, }, };After the first chunk arrives from the server, the data looks like the following:
{ greeting: { __typename: "Greeting", message: "Hello, John", recipient: { __typename: "Person", name: "John Doe", email: "[email protected]", }, }, };In this case, the combination of the first chunk and the partial data in the cache now fulfills the data requirements of the query. Even though the server is still streaming data (
NetworkStatus.streaming), we can report this asdataState: "complete"since it is safe to access data on all fields.This change also means
@streamqueries by definition fulfill the data requirements of the query after the first chunk arrives since@streamoperates on lists and contains no data holes.@streamqueries now accurately reportdataStateascompleteorpartial, depending on whether the list mixes partial data with streamed list items.As a result of this change, some cases where you'd previously see
dataStatereported as"streaming"are now reported aspartialorcomplete.If you use
dataStateto determine whether an incremental request is still in-flight, please usenetworkStatusinstead to check forNetworkStatus.streaming.dataStateis type narrowing feature and not intended to report the network status. - Scenario 1: partial data inside a
Patch Changes
-
#13324
0abd8deThanks @jerelmiller! - Fix an issue where fieldreadfunctions were not applied to intermediate results while streaming@deferresponses.cache.diffran thereadfunctions, but the transformed values were only applied to the emitted result when the updated cache result was considered complete. Intermediate chunks whose only holes were at@deferboundaries now correctly return the result of fieldreadfunctions.new InMemoryCache({ typePolicies: { Greeting: { fields: { message: { read: (message) => message.toUpperCase(), }, }, }, }, }); // query GreetingQuery { // greeting { // message // ... @defer { // recipient { name } // } // } // } // First chunk previously returned: // { greeting: { message: "Hello world" } } // // Now correctly returns while still streaming: // { greeting: { message: "HELLO WORLD" } } -
#13324
0abd8deThanks @jerelmiller! - Fix an issue with@streamqueries when usingreturnPartialData: truewhere the streamed list was truncated after the first incremental chunk when the list contained partial cache data. The list is no longer truncated and partial list items are now retained as incremental chunks arrive. ThedataStateis now reported aspartialuntil the server has streamed enough of the list so that each list item fully satisfies the query.This change also updates
@streamqueries so that they reported withdataState: "completeinstead of"streaming"since it is safe to access all fields in the response.
-
- 4.3.0-alpha.230 Jun 2026pre-release
Release notes
Open source →Minor Changes
-
#13274
7b10078Thanks @jerelmiller! - AddsScalar.fromGraphQLScalarTypehelper to create aScalarinstance from an existing graphql.jsGraphQLScalarType.import { GraphQLScalarType } from "graphql"; import { Scalar } from "@apollo/client"; const dateTimeScalarType = new GraphQLScalarType<Date, string>({ // ... }); const dateTimeScalar = Scalar.fromGraphQLScalarType(dateTimeScalarType, { is: (value) => value instanceof Date, }); -
#13252
ed86234Thanks @jerelmiller! - Adds the plumbing and types implementation for declaring custom scalars and configuring custom scalars inInMemoryCache.You can declare custom scalar types with declaration merging on the
ApolloCache.Scalarsinterface:// apollo.d.ts import "@apollo/client"; declare module "@apollo/client" { namespace ApolloCache { interface Scalars { Date: { serialized: string; parsed: Date }; } } }This enables the
scalarsoption inInMemoryCache:import { Scalar } from "@apollo/client"; const cache = new InMemoryCache({ scalars: { Date: new Scalar({ parse: (dateString) => new Date(dateString), serialize: (date) => date.toISOString(), is: (value) => value instanceof Date, }), }, }); -
#13259
ccaf686Thanks @jerelmiller! - Adds ascalaroption toInMemoryCachefield policies that tells the cache which scalar to use when parsing or serializing the field value.import { Scalar } from "@apollo/client"; new InMemoryCache({ scalars: { DateTime: new Scalar({ parse: (dateString) => new Date(dateString), serialize: (date) => date.toISOString(), }), }, typePolicies: { Event: { fields: { startTime: { // Parse this field using the DateTime scalar scalar: "DateTime", }, }, }, }, });This scalar definition is now used to properly parse or serialize the field value for cache reads and writes as well as
cache.extract()andcache.restore(). -
#13273
0886de1Thanks @jerelmiller! - Automatically serialize variables that include custom scalar values. This includes cache reads and writes as well as requests to the network.For more complex input objects, a new
inputObjectsoption is available toInMemoryCachethat specifies where nested scalar fields are found.const cache = new InMemoryCache({ scalars: { DateTime: new Scalar({ parse: (value) => new Date(value), serialize: (value) => value.toISOString(), is: (value) => value instanceof Date, }), }, inputObjects: { EventFilter: { fields: { date: "DateTime", }, }, }, }); const client = new ApolloClient({ cache, link }); await client.query({ query: gql` query Event($filter: EventFilter!) { event(filter: $filter) { name } } `, variables: { filter: { date: new Date("2026-01-01T00:00:00.000Z"), }, }, }); // The link receives: // { filter: { date: "2026-01-01T00:00:00.000Z" } } -
#13252
ed86234Thanks @jerelmiller! - Adds thegetScalarabstract method toApolloCachethat cache subclasses override to provide scalar behavior to Apollo Client. Defaults to unconditionally returnundefinedif not specified.
-
- 4.3.0-alpha.111 Jun 2026pre-release
Release notes
Open source →Patch Changes
- #13268
419e2b5Thanks @DaleSeo! - Align the remaining cache generic constraints withCache.Implementation. The deprecated React mutation types (MutationHookOptions,MutationFunctionOptions,MutationTuple) and the internalInternalRefetchQueriesOptionsandQueryInfotypes still constrained their cache type parameter toApolloCache, so they now match the rest of the overridable cache API.
- #13268
- 4.3.0-alpha.09 Jun 2026pre-release
Release notes
Open source →Minor Changes
-
#13250
bad7035Thanks @jerelmiller! - Add the ability to define the cache type for the client.client.cachecurrently returnsApolloCacheas the cache type regardless of what cache you've provided toApolloClient.Declare the cache type using the
cacheproperty in theTypeOverridesinterface to set the cache implementation used for the client.// apollo.d.ts import type { InMemoryCache } from "@apollo/client"; declare module "@apollo/client" { export interface TypeOverrides { cache: InMemoryCache; } }Now anywhere
cacheis accessible, the type is the declared cache type:client.cache; // ^? InMemoryCache client.mutate({ update: (cache) => { // ^? InMemoryCache }, });[!NOTE] Setting a cache type enforces that cache type in the
cacheoption for theApolloClientconstructor.
-
- 4.2.1214 Aug 2026
Release notes
Open source →Patch Changes
- #13400
56ca81bThanks @QiRaining! - Preserve multi-byte UTF-8 characters split across multipart response chunks.
Additional notes2 sources agree
Open source →Patch Changes
- #13400
56ca81bThanks @QiRaining! - Preserve multi-byte UTF-8 characters split across multipart response chunks.
- #13400
- 4.2.1111 Aug 2026
Release notes3 sources agree
Open source →Patch Changes
-
#13398
3dd3e9aThanks @phryneas! - Fix type signature of someDocumentationTypesto fix their display in our documentation. -
#13392
d4f0771Thanks @jerelmiller! - Add a development-only warning when a network result is written to the cache but reading the query back from the cache returns a partial result. This usually points at amergeorreadfunction that did not repair missing fields in the cache, which prevents Apollo Client from applying the cache result to the data received by the network.
-
- 4.2.105 Aug 2026
Release notes3 sources agree
Open source →Patch Changes
-
#13385
bfb674eThanks @jerelmiller! - Fix accidental widening of theclient.mutatereturn type whenoptimisticResponsewas present. -
#13382
365373eThanks @jerelmiller! - Fix result types widened when a query's variables had constant types (e.g.TypedDocumentNode<Data, { type: "main" }>). This caused options such asreturnPartialDataorerrorPolicyto be reported as their widened types (e.g.boolean,ErrorPolicy) instead of the value that was passed which returned the wrongdataanddataStatetypes. -
#13382
365373eThanks @jerelmiller! - Fix issue where unknown options were permitted by TypeScript when passed alongside a valid option to APIs with modern signatures. -
#13383
5840f50Thanks @jerelmiller! - Update the return type ofrefetch,fetchMoreanduseLazyQuery'sexecutefunction on the providederrorPolicy. Previously these APIs all used the default type which typeddataasTData | undefinedanderrorasErrorLike | undefined.
-
- 4.2.930 Jul 2026
Release notes3 sources agree
Open source →Patch Changes
-
#13364
2f383e7Thanks @atharv-sys32! - Fix a bug where GraphQL variable default values were not applied during cache reads when variables with defaults were explicitly set toundefined. This caused@include/@skipdirectives to throw "Invalid variable referenced" errors when the variable was passed asundefinedinstead of being omitted entirely. -
#13367
2b39cc8Thanks @jerelmiller! - Fix an issue where some@exportqueries would not react to cache updates when the fields keyed by exported variables were updated.
-
- 4.2.823 Jul 2026
Release notes3 sources agree
Open source →Patch Changes
- #13349
501a33bThanks @jerelmiller! - Prevent thesetTimeoutinconnectToDevtoolsthat shows the devtools suggestion from firing when the user agent does not match Chrome or Firefox. This check was previously done inside thesetTimeoutwhich meant the timer was scheduled for environments where we'd never show the message anyways. For test environments, this could cause flaky tests when thatsetTimeoutoutlived the tests and ran after any virtual DOM was torn down and removed.
- #13349
- 4.2.713 Jul 2026
Release notes3 sources agree
Open source →Patch Changes
- #13320
538c906Thanks @jerelmiller! - Cleanup some unused internals. Please file an issue if you notice anything change.
- #13320
- 4.2.66 Jul 2026
Release notes3 sources agree
Open source →Patch Changes
-
#13315
a406cc9Thanks @fallintoplace! - Prevent relay multipart subscriptions from issuing a fetch request after serializing the request body fails. -
#13307
abd0781Thanks @wolfie! - Speed up cache writes by avoiding a full ASTvisitof every written field to detect@stream. The check now runs only when the result carries stream info, and only inspects the field node's own directives. As a result, fields that merely contain@streamon a nested field are no longer treated as streamed themselves and now overwrite existing lists like regular fields instead of merging chunk-wise.
-
- 4.2.51 Jul 2026
- 4.2.430 Jun 2026
Release notes3 sources agree
Open source →Patch Changes
- #13281
e4df809Thanks @jerelmiller! - Fixes an issue whereclient.readFragmentandclient.readQueryignored theoptimisticoption when passed in the options object.
- #13281
- 4.2.38 Jun 2026
Release notes3 sources agree
Open source →Patch Changes
- #13254
66e9dfcThanks @jerelmiller! - Add support forgraphqlv17 as a valid peer dependency.
- #13254
- 4.2.23 Jun 2026
Release notes3 sources agree
Open source →Patch Changes
- #13184
c207b88Thanks @audrius-savickas! - Preserve referential equality of masked data on refetch when the result is deeply equal to the previous result.
- #13184
- 4.2.12 Jun 2026
Release notes3 sources agree
Open source →Patch Changes
- #13248
062ffe3Thanks @jerelmiller! - Fixes an issue whereuseLazyQuerywould not apply a changedpollIntervalbetween renders.
- #13248
- 4.2.020 May 2026
Release notes3 sources agree
Open source →Minor Changes
-
#13132
f3ce805Thanks @phryneas! - Introduce "classic" and "modern" method and hook signatures.Apollo Client 4.2 introduces two signature styles for methods and hooks. All signatures previously present are now "classic" signatures, and a new set of "modern" signatures are added alongside them.
Classic signatures are the default and are identical to the signatures before Apollo Client 4.2, preserving backward compatibility. Classic signatures still work with manually specified TypeScript generics (e.g.,
useSuspenseQuery<MyData>(...)). However, manually specifying generics has been discouraged for a long time—instead, we recommend usingTypedDocumentNodeto automatically infer types, which provides more accurate results without any manual annotations.Modern signatures automatically incorporate your declared
defaultOptionsinto return types, providing more accurate types. Modern signatures infer types from the document node and do not support manually passing generic type arguments; TypeScript will produce a type error if you attempt to do so.Methods and hooks automatically switch to modern signatures the moment any non-optional property is declared in
DeclareDefaultOptions. The switch happens across all methods and hooks globally:// apollo.d.ts import "@apollo/client"; declare module "@apollo/client" { namespace ApolloClient { namespace DeclareDefaultOptions { interface WatchQuery { errorPolicy: "all"; // non-optional → modern signatures activated automatically } } } }Users can also manually switch to modern signatures without declaring any
defaultOptions, for example when wanting accurate type inference without relying on globaldefaultOptions:// apollo.d.ts import "@apollo/client"; declare module "@apollo/client" { export interface TypeOverrides { signatureStyle: "modern"; } }Users can do a global
DeclareDefaultOptionstype augmentation and then manually switch back to "classic" for migration purposes:// apollo.d.ts import "@apollo/client"; declare module "@apollo/client" { export interface TypeOverrides { signatureStyle: "classic"; } }Note that this is not recommended for long-term use. When combined with
DeclareDefaultOptions, switching back to classic results in the same incorrect types as before Apollo Client 4.2—methods and hooks will not reflect thedefaultOptionsyou've declared. -
#13130
dd12231Thanks @jerelmiller! - Improve the accuracy ofclient.queryreturn type to better detect the currenterrorPolicy. Thedataproperty is no longer nullable when theerrorPolicyisnone. This makes it possible to remove theundefinedchecks or optional chaining in most cases. -
#13210
1f9a428Thanks @jerelmiller! - Add support for automatic event-based refetching, such as window focus.The
RefetchEventManagerclass handles automatic refetches in response to events. Apollo Client provides built-in sources for window focus and network reconnect aswindowFocusSourceandonlineSource.Event refetching is fully opt-in. Create and pass a
RefetchEventManagerinstance to theApolloClientconstructor to activate the event listeners.import { ApolloClient, InMemoryCache, RefetchEventManager, windowFocusSource, onlineSource, } from "@apollo/client"; const client = new ApolloClient({ link, cache: new InMemoryCache(), refetchEventManager: new RefetchEventManager({ sources: { // Refetch when window is focused windowFocus: windowFocusSource, // Refetch when the user comes back online online: onlineSource, }, }), });By default, all active queries refetch when the events fire. Queries can opt out per-event or disable all event refetches:
// Skip refetch on window focus for this query, but keep `online` useQuery(QUERY, { refetchOn: { windowFocus: false }, }); // Disable all event-driven refetches for this query useQuery(OTHER_QUERY, { refetchOn: false, }); // Enable every event for this query, regardless of defaultOptions useQuery(LIVE_DASHBOARD, { refetchOn: true, }); // Dynamically enable or disable a refetch when the event fires useQuery(LIVE_DASHBOARD, { refetchOn: ({ source, payload }) => { if (source === "windowFocus") { // payload is the data associated with the event return someCondition(payload); } return true; }, }); // Dynamically enable or disable a refetch for a specific event useQuery(LIVE_DASHBOARD, { refetchOn: { windowFocus: ({ payload }) => { // payload is the data associated with the event return someCondition(payload); }, }, });To enable per-query opt-in rather than opt-out, set
defaultOptions.watchQuery.refetchOntofalseand enable it per-query instead.const client = new ApolloClient({ link, cache, refetchEventManager: new RefetchEventManager({ sources: { windowFocus: windowFocusSource }, }), defaultOptions: { watchQuery: { refetchOn: false }, }, }); // Only this query refetches on window focus useQuery(DASHBOARD_QUERY, { refetchOn: { windowFocus: true } });When
defaultOptions.watchQuery.refetchOnand per-queryrefetchOnoptions are provided, the objects are merged together.Custom events
You can also add your own custom events that trigger refetches. Register your event name and payload type using TypeScript module augmentation, then provide a source function that returns an Observable. The source's emitted value becomes the event's
payload.import { Observable } from "@apollo/client"; import { filter } from "rxjs"; import { AppState, AppStateStatus, Platform } from "react-native"; declare module "@apollo/client" { interface RefetchEvents { reactNativeAppStatus: AppStateStatus; } } const refetchEventManager = new RefetchEventManager({ sources: { reactNativeAppStatus: () => { return new Observable((observer) => { const subscription = AppState.addEventListener("change", (status) => { observer.next(status); }); return () => subscription.remove(); }).pipe( filter((status) => Platform.OS !== "web" && status === "active") ); }, }, }); // Disable per-query by setting the event to false useQuery(QUERY, { refetchOn: { reactNativeAppStatus: false } });Manually trigger an event refetch
Refetches can be triggered imperatively by calling
emitwith the event name and its payload (if any).refetchEventManager.emit("reactNativeAppStatus", "active");Sourceless events
A source that has no automatic detection logic but still wants imperative
emitsupport can be declared astrue. Type the event asvoidto omit the payload argument.declare module "@apollo/client" { interface RefetchEvents { userTriggered: void; } } const refetchEventManager = new RefetchEventManager({ sources: { userTriggered: true }, }); refetchEventManager.emit("userTriggered");Note: Calling
emiton an event without a registered source will log a warning and result in a no-op.Custom handlers
When an event fires, the default handler calls
client.refetchQueries({ include: "active" })filtered by each query'srefetchOnsetting. You can override the handler for an event to add your own custom filtering. For example, to refetch all queries, includingstandbyqueries, define a handler for the event:const refetchEventManager = new RefetchEventManager({ // ... handlers: { userTriggered: ({ client, source, payload, matchesRefetchOn }) => { return client.refetchQueries({ include: "all", onQueryUpdated: (observableQuery) => { return matchesRefetchOn(observableQuery); }, }); }, }, });Handlers must return either a
RefetchQueriesResultorvoid. Returningvoidskips refetching for the event. -
#13232
f1b541fThanks @jerelmiller! - Version bump torc. -
#13206
08fccabThanks @jerelmiller! - Extend thedefaultOptionstype-safety work toclient.mutateanduseMutation.The
errorPolicyoption now flows through to the result types for mutations in the same way it already does for queries:ApolloClient.MutateResult<TData, TErrorPolicy>mapserrorPolicyto the concrete shape ofdataanderror:"none"→{ data: TData; error?: never }"all"→{ data: TData | undefined; error?: ErrorLike }"ignore"→{ data: TData | undefined; error?: never }
client.mutateanduseMutationpick up the declareddefaultOptions.mutate.errorPolicyand the expliciterrorPolicyon each call to narrow return types accordingly.useMutation.Result.erroris narrowed toundefinedwhenerrorPolicyis"ignore", sinceclient.mutatenever resolves with an error in that case.
DeclareDefaultOptions.Mutatealready acceptederrorPolicy; the new behavior is that once you declare it, hook and method return types reflect it:// apollo.d.ts import "@apollo/client"; declare module "@apollo/client" { namespace ApolloClient { namespace DeclareDefaultOptions { interface Mutate { errorPolicy: "all"; } } } }const result = await client.mutate({ mutation: MUTATION }); result.data; // ^? TData | undefined result.error; // ^? ErrorLike | undefinedSetting
errorPolicyon an individual call overrides the default for that call's return type. -
#13222
b93c172Thanks @jerelmiller! - Extend thedefaultOptionstype-safety work topreloadQuery(returned fromcreateQueryPreloader). Defaults declared inDeclareDefaultOptions.WatchQuerynow work withpreloadQueryto ensure thePreloadedQueryRef's data states are correctly set.// apollo.d.ts import "@apollo/client"; declare module "@apollo/client" { namespace ApolloClient { namespace DeclareDefaultOptions { interface WatchQuery { errorPolicy: "all"; } } } }const preloadQuery = createQueryPreloader(client); const queryRef = preloadQuery(QUERY); // ^? PreloadedQueryRef<TData, TVariables, "complete" | "streaming" | "empty"> -
#13132
f3ce805Thanks @phryneas! - Synchronize method and hook return types withdefaultOptions.Prior to this change, the following code snippet would always apply:
declare const MY_QUERY: TypedDocumentNode<TData, TVariables>; const result1 = useSuspenseQuery(MY_QUERY); result1.data; // ^? TData const result2 = useSuspenseQuery(MY_QUERY, { errorPolicy: "all" }); result2.data; // ^? TData | undefinedWhile these types are generally correct, if you were to set
errorPolicy: 'all'as a default option, the type ofresult.datafor the first query would remainTDatainstead of changing toTData | undefinedto match the runtime behavior.We are now enforcing that certain
defaultOptionstypes need to be registered globally. This means that if you want to useerrorPolicy: 'all'as a default option for a query, you will need to register its type like this:// apollo.d.ts import "@apollo/client"; declare module "@apollo/client" { namespace ApolloClient { namespace DeclareDefaultOptions { interface WatchQuery { // possible global-registered values: // * `errorPolicy` // * `returnPartialData` errorPolicy: "all"; } interface Query { // possible global-registered values: // * `errorPolicy` } interface Mutate { // possible global-registered values: // * `errorPolicy` } } } }Once this type declaration is in place, the type of
result.datain the above example will correctly be changed toTData | undefined, reflecting the possibility that if an error occurs,datamight beundefined. Manually specifyinguseSuspenseQuery(MY_QUERY, { errorPolicy: "none" });changesresult.datatoTDatato reflect the local override.This change means that you will need to declare your default options types in order to use
defaultOptionswithApolloClient, otherwise you will see a TypeScript error.Without the type declaration, the following (previously valid) code will now error:
new ApolloClient({ link: ApolloLink.empty(), cache: new InMemoryCache(), defaultOptions: { watchQuery: { // results in a type error: // Type '"all"' is not assignable to type '"A default option for watchQuery.errorPolicy must be declared in ApolloClient.DeclareDefaultOptions before usage. See https://www.apollographql.com/docs/react/data/typescript#declaring-default-options-for-type-safety."'. errorPolicy: "all", }, }, });If you are creating multiple instances of Apollo Client with conflicting default options and you cannot register a single
defaultOptionsvalue as a result, you can relax the constraints by declaring those options as union types covering all values used by all clients. The properties can be required (to enforce them indefaultOptions) or optional (if some constructor calls won't pass a value):// apollo.d.ts import "@apollo/client"; declare module "@apollo/client" { export namespace ApolloClient { export namespace DeclareDefaultOptions { interface WatchQuery { errorPolicy?: "none" | "all" | "ignore"; returnPartialData?: boolean; } interface Query { errorPolicy?: "none" | "all" | "ignore"; } interface Mutate { errorPolicy?: "none" | "all" | "ignore"; } } } }With this declaration, the
ApolloClientconstructor accepts any of those values indefaultOptions. The tradeoff is that hook and method return types become more generic. For example, callinguseSuspenseQuerywithout an expliciterrorPolicywill return a result typed as if all error policies are possible, since TypeScript can't know which specific value your instance uses at runtime.Note that making a property optional (
errorPolicy?:) is equivalent to adding the TypeScript default value ("none") to the union. SoerrorPolicy?: "all" | "ignore"has the same effect on return types aserrorPolicy: "none" | "all" | "ignore", because TypeScript assumes the option could also be absent (i.e.,"none").You can also use a partial union that only lists the values you actually use. For example, if you only ever use
"all"or"ignore", declareerrorPolicy: "all" | "ignore"(required) to keep the union narrow and avoid unused values broadening your signatures unnecessarily.
Patch Changes
-
#13217
790f987Thanks @jerelmiller! - Fix the deprecation for the classic signatures for function overloads that rely on type inference from aTypedDocumentNode. The deprecation now only applies to classic signatures that provide explicit type arguments to encourage the use ofTypedDocumentNode. -
#13166
0537d97Thanks @jerelmiller! - Release changes in 4.1.5 and 4.1.6. -
#13215
54c9eb7Thanks @jerelmiller! - Ensure the options object for theuseQuery,useSuspenseQuery, anduseBackgroundQueryhooks provide proper IntelliSense suggestions. -
#13229
9a7f65aThanks @jerelmiller! - FixrefetchOnmerging whendefaultOptions.watchQuery.refetchOnis set to a non-object value (false,true, or a function) and the per-queryrefetchOnis an object. Previously the per-query object completely replaced the default so unspecified events fell back to "enabled" regardless of the default.The
defaultOptionsvalue now applies to any event the per-query object does not explicitly configure:false- unspecified events stay disabledtrue- unspecified events refetch- Callback function - the function is called for unspecified events to determine whether to refetch
const client = new ApolloClient({ // ... defaultOptions: { watchQuery: { refetchOn: false, }, }, }); // Only `windowFocus` refetches. Other events stay disabled per the default. useQuery(QUERY, { refetchOn: { windowFocus: true } }); -
#13230
b25b659Thanks @jerelmiller! - Add the ability to override the default event handler onRefetchEventManager. The default handler runs when no per-source handler is configured for an event. Provide a custom handler via thedefaultHandlerconstructor option or thesetDefaultEventHandlerinstance method.new RefetchEventManager({ defaultHandler: ({ client, matchesRefetchOn }) => { return client.refetchQueries({ include: "all", onQueryUpdated: matchesRefetchOn, }); }, });
-
- 4.2.0-rc.011 May 2026pre-release
Release notes3 sources agree
Open source →Minor Changes
- #13232
f1b541fThanks @jerelmiller! - Version bump torc.
- #13232
- 4.2.0-alpha.88 May 2026pre-release
Release notes3 sources agree
Open source →Patch Changes
-
#13229
9a7f65aThanks @jerelmiller! - FixrefetchOnmerging whendefaultOptions.watchQuery.refetchOnis set to a non-object value (false,true, or a function) and the per-queryrefetchOnis an object. Previously the per-query object completely replaced the default so unspecified events fell back to "enabled" regardless of the default.The
defaultOptionsvalue now applies to any event the per-query object does not explicitly configure:false- unspecified events stay disabledtrue- unspecified events refetch- Callback function - the function is called for unspecified events to determine whether to refetch
const client = new ApolloClient({ // ... defaultOptions: { watchQuery: { refetchOn: false, }, }, }); // Only `windowFocus` refetches. Other events stay disabled per the default. useQuery(QUERY, { refetchOn: { windowFocus: true } }); -
#13230
b25b659Thanks @jerelmiller! - Add the ability to override the default event handler onRefetchEventManager. The default handler runs when no per-source handler is configured for an event. Provide a custom handler via thedefaultHandlerconstructor option or thesetDefaultEventHandlerinstance method.new RefetchEventManager({ defaultHandler: ({ client, matchesRefetchOn }) => { return client.refetchQueries({ include: "all", onQueryUpdated: matchesRefetchOn, }); }, });
-
- 4.2.0-alpha.75 May 2026pre-release
Release notes3 sources agree
Open source →Minor Changes
-
#13222
b93c172Thanks @jerelmiller! - Extend thedefaultOptionstype-safety work topreloadQuery(returned fromcreateQueryPreloader). Defaults declared inDeclareDefaultOptions.WatchQuerynow work withpreloadQueryto ensure thePreloadedQueryRef's data states are correctly set.// apollo.d.ts import "@apollo/client"; declare module "@apollo/client" { namespace ApolloClient { namespace DeclareDefaultOptions { interface WatchQuery { errorPolicy: "all"; } } } }const preloadQuery = createQueryPreloader(client); const queryRef = preloadQuery(QUERY); // ^? PreloadedQueryRef<TData, TVariables, "complete" | "streaming" | "empty">
-
- 4.2.0-alpha.65 May 2026pre-release
Release notes3 sources agree
Open source →Patch Changes
- #13217
790f987Thanks @jerelmiller! - Fix the deprecation for the classic signatures for function overloads that rely on type inference from aTypedDocumentNode. The deprecation now only applies to classic signatures that provide explicit type arguments to encourage the use ofTypedDocumentNode.
- #13217
- 4.2.0-alpha.51 May 2026pre-release
Release notes3 sources agree
Open source →Patch Changes
- #13215
54c9eb7Thanks @jerelmiller! - Ensure the options object for theuseQuery,useSuspenseQuery, anduseBackgroundQueryhooks provide proper IntelliSense suggestions.
- #13215
- 4.2.0-alpha.429 Apr 2026pre-release
Release notes3 sources agree
Open source →Minor Changes
-
#13210
1f9a428Thanks @jerelmiller! - Add support for automatic event-based refetching, such as window focus.The
RefetchEventManagerclass handles automatic refetches in response to events. Apollo Client provides built-in sources for window focus and network reconnect aswindowFocusSourceandonlineSource.Event refetching is fully opt-in. Create and pass a
RefetchEventManagerinstance to theApolloClientconstructor to activate the event listeners.import { ApolloClient, InMemoryCache, RefetchEventManager, windowFocusSource, onlineSource, } from "@apollo/client"; const client = new ApolloClient({ link, cache: new InMemoryCache(), refetchEventManager: new RefetchEventManager({ sources: { // Refetch when window is focused windowFocus: windowFocusSource, // Refetch when the user comes back online online: onlineSource, }, }), });By default, all active queries refetch when the events fire. Queries can opt out per-event or disable all event refetches:
// Skip refetch on window focus for this query, but keep `online` useQuery(QUERY, { refetchOn: { windowFocus: false }, }); // Disable all event-driven refetches for this query useQuery(OTHER_QUERY, { refetchOn: false, }); // Enable every event for this query, regardless of defaultOptions useQuery(LIVE_DASHBOARD, { refetchOn: true, }); // Dynamically enable or disable a refetch when the event fires useQuery(LIVE_DASHBOARD, { refetchOn: ({ source, payload }) => { if (source === "windowFocus") { // payload is the data associated with the event return someCondition(payload); } return true; }, }); // Dynamically enable or disable a refetch for a specific event useQuery(LIVE_DASHBOARD, { refetchOn: { windowFocus: ({ payload }) => { // payload is the data associated with the event return someCondition(payload); }, }, });To enable per-query opt-in rather than opt-out, set
defaultOptions.watchQuery.refetchOntofalseand enable it per-query instead.const client = new ApolloClient({ link, cache, refetchEventManager: new RefetchEventManager({ sources: { windowFocus: windowFocusSource }, }), defaultOptions: { watchQuery: { refetchOn: false }, }, }); // Only this query refetches on window focus useQuery(DASHBOARD_QUERY, { refetchOn: { windowFocus: true } });When
defaultOptions.watchQuery.refetchOnand per-queryrefetchOnoptions are provided, the objects are merged together.Custom events
You can also add your own custom events that trigger refetches. Register your event name and payload type using TypeScript module augmentation, then provide a source function that returns an Observable. The source's emitted value becomes the event's
payload.import { Observable } from "@apollo/client"; import { filter } from "rxjs"; import { AppState, AppStateStatus, Platform } from "react-native"; declare module "@apollo/client" { interface RefetchEvents { reactNativeAppStatus: AppStateStatus; } } const refetchEventManager = new RefetchEventManager({ sources: { reactNativeAppStatus: () => { return new Observable((observer) => { const subscription = AppState.addEventListener("change", (status) => { observer.next(status); }); return () => subscription.remove(); }).pipe( filter((status) => Platform.OS !== "web" && status === "active") ); }, }, }); // Disable per-query by setting the event to false useQuery(QUERY, { refetchOn: { reactNativeAppStatus: false } });Manually trigger an event refetch
Refetches can be triggered imperatively by calling
emitwith the event name and its payload (if any).refetchEventManager.emit("reactNativeAppStatus", "active");Sourceless events
A source that has no automatic detection logic but still wants imperative
emitsupport can be declared astrue. Type the event asvoidto omit the payload argument.declare module "@apollo/client" { interface RefetchEvents { userTriggered: void; } } const refetchEventManager = new RefetchEventManager({ sources: { userTriggered: true }, }); refetchEventManager.emit("userTriggered");Note: Calling
emiton an event without a registered source will log a warning and result in a no-op.Custom handlers
When an event fires, the default handler calls
client.refetchQueries({ include: "active" })filtered by each query'srefetchOnsetting. You can override the handler for an event to add your own custom filtering. For example, to refetch all queries, includingstandbyqueries, define a handler for the event:const refetchEventManager = new RefetchEventManager({ // ... handlers: { userTriggered: ({ client, source, payload, matchesRefetchOn }) => { return client.refetchQueries({ include: "all", onQueryUpdated: (observableQuery) => { return matchesRefetchOn(observableQuery); }, }); }, }, });Handlers must return either a
RefetchQueriesResultorvoid. Returningvoidskips refetching for the event.
-
- 4.2.0-alpha.327 Apr 2026pre-release
Release notes3 sources agree
Open source →Minor Changes
-
#13206
08fccabThanks @jerelmiller! - Extend thedefaultOptionstype-safety work toclient.mutateanduseMutation.The
errorPolicyoption now flows through to the result types for mutations in the same way it already does for queries:ApolloClient.MutateResult<TData, TErrorPolicy>mapserrorPolicyto the concrete shape ofdataanderror:"none"→{ data: TData; error?: never }"all"→{ data: TData | undefined; error?: ErrorLike }"ignore"→{ data: TData | undefined; error?: never }
client.mutateanduseMutationpick up the declareddefaultOptions.mutate.errorPolicyand the expliciterrorPolicyon each call to narrow return types accordingly.useMutation.Result.erroris narrowed toundefinedwhenerrorPolicyis"ignore", sinceclient.mutatenever resolves with an error in that case.
DeclareDefaultOptions.Mutatealready acceptederrorPolicy; the new behavior is that once you declare it, hook and method return types reflect it:// apollo.d.ts import "@apollo/client"; declare module "@apollo/client" { namespace ApolloClient { namespace DeclareDefaultOptions { interface Mutate { errorPolicy: "all"; } } } }const result = await client.mutate({ mutation: MUTATION }); result.data; // ^? TData | undefined result.error; // ^? ErrorLike | undefinedSetting
errorPolicyon an individual call overrides the default for that call's return type.
-
- 4.2.0-alpha.221 Apr 2026pre-release
Release notes3 sources agree
Open source →Minor Changes
-
#13132
f3ce805Thanks @phryneas! - Introduce "classic" and "modern" method and hook signatures.Apollo Client 4.2 introduces two signature styles for methods and hooks. All signatures previously present are now "classic" signatures, and a new set of "modern" signatures are added alongside them.
Classic signatures are the default and are identical to the signatures before Apollo Client 4.2, preserving backward compatibility. Classic signatures still work with manually specified TypeScript generics (e.g.,
useSuspenseQuery<MyData>(...)). However, manually specifying generics has been discouraged for a long time—instead, we recommend usingTypedDocumentNodeto automatically infer types, which provides more accurate results without any manual annotations.Modern signatures automatically incorporate your declared
defaultOptionsinto return types, providing more accurate types. Modern signatures infer types from the document node and do not support manually passing generic type arguments; TypeScript will produce a type error if you attempt to do so.Methods and hooks automatically switch to modern signatures the moment any non-optional property is declared in
DeclareDefaultOptions. The switch happens across all methods and hooks globally:// apollo.d.ts import "@apollo/client"; declare module "@apollo/client" { namespace ApolloClient { namespace DeclareDefaultOptions { interface WatchQuery { errorPolicy: "all"; // non-optional → modern signatures activated automatically } } } }Users can also manually switch to modern signatures without declaring any
defaultOptions, for example when wanting accurate type inference without relying on globaldefaultOptions:// apollo.d.ts import "@apollo/client"; declare module "@apollo/client" { export interface TypeOverrides { signatureStyle: "modern"; } }Users can do a global
DeclareDefaultOptionstype augmentation and then manually switch back to "classic" for migration purposes:// apollo.d.ts import "@apollo/client"; declare module "@apollo/client" { export interface TypeOverrides { signatureStyle: "classic"; } }Note that this is not recommended for long-term use. When combined with
DeclareDefaultOptions, switching back to classic results in the same incorrect types as before Apollo Client 4.2—methods and hooks will not reflect thedefaultOptionsyou've declared. -
#13132
f3ce805Thanks @phryneas! - Synchronize method and hook return types withdefaultOptions.Prior to this change, the following code snippet would always apply:
declare const MY_QUERY: TypedDocumentNode<TData, TVariables>; const result1 = useSuspenseQuery(MY_QUERY); result1.data; // ^? TData const result2 = useSuspenseQuery(MY_QUERY, { errorPolicy: "all" }); result2.data; // ^? TData | undefinedWhile these types are generally correct, if you were to set
errorPolicy: 'all'as a default option, the type ofresult.datafor the first query would remainTDatainstead of changing toTData | undefinedto match the runtime behavior.We are now enforcing that certain
defaultOptionstypes need to be registered globally. This means that if you want to useerrorPolicy: 'all'as a default option for a query, you will need to register its type like this:// apollo.d.ts import "@apollo/client"; declare module "@apollo/client" { namespace ApolloClient { namespace DeclareDefaultOptions { interface WatchQuery { // possible global-registered values: // * `errorPolicy` // * `returnPartialData` errorPolicy: "all"; } interface Query { // possible global-registered values: // * `errorPolicy` } interface Mutate { // possible global-registered values: // * `errorPolicy` } } } }Once this type declaration is in place, the type of
result.datain the above example will correctly be changed toTData | undefined, reflecting the possibility that if an error occurs,datamight beundefined. Manually specifyinguseSuspenseQuery(MY_QUERY, { errorPolicy: "none" });changesresult.datatoTDatato reflect the local override.This change means that you will need to declare your default options types in order to use
defaultOptionswithApolloClient, otherwise you will see a TypeScript error.Without the type declaration, the following (previously valid) code will now error:
new ApolloClient({ link: ApolloLink.empty(), cache: new InMemoryCache(), defaultOptions: { watchQuery: { // results in a type error: // Type '"all"' is not assignable to type '"A default option for watchQuery.errorPolicy must be declared in ApolloClient.DeclareDefaultOptions before usage. See https://www.apollographql.com/docs/react/data/typescript#declaring-default-options-for-type-safety."'. errorPolicy: "all", }, }, });If you are creating multiple instances of Apollo Client with conflicting default options and you cannot register a single
defaultOptionsvalue as a result, you can relax the constraints by declaring those options as union types covering all values used by all clients. The properties can be required (to enforce them indefaultOptions) or optional (if some constructor calls won't pass a value):// apollo.d.ts import "@apollo/client"; declare module "@apollo/client" { export namespace ApolloClient { export namespace DeclareDefaultOptions { interface WatchQuery { errorPolicy?: "none" | "all" | "ignore"; returnPartialData?: boolean; } interface Query { errorPolicy?: "none" | "all" | "ignore"; } interface Mutate { errorPolicy?: "none" | "all" | "ignore"; } } } }With this declaration, the
ApolloClientconstructor accepts any of those values indefaultOptions. The tradeoff is that hook and method return types become more generic. For example, callinguseSuspenseQuerywithout an expliciterrorPolicywill return a result typed as if all error policies are possible, since TypeScript can't know which specific value your instance uses at runtime.Note that making a property optional (
errorPolicy?:) is equivalent to adding the TypeScript default value ("none") to the union. SoerrorPolicy?: "all" | "ignore"has the same effect on return types aserrorPolicy: "none" | "all" | "ignore", because TypeScript assumes the option could also be absent (i.e.,"none").You can also use a partial union that only lists the values you actually use. For example, if you only ever use
"all"or"ignore", declareerrorPolicy: "all" | "ignore"(required) to keep the union narrow and avoid unused values broadening your signatures unnecessarily.
-
- 4.2.0-alpha.15 Mar 2026pre-release
Release notes3 sources agree
Open source →Patch Changes
- #13166
0537d97Thanks @jerelmiller! - Release changes in 4.1.5 and 4.1.6.
- #13166
- 4.2.0-alpha.013 Feb 2026pre-release
Release notes3 sources agree
Open source →Minor Changes
- #13130
dd12231Thanks @jerelmiller! - Improve the accuracy ofclient.queryreturn type to better detect the currenterrorPolicy. Thedataproperty is no longer nullable when theerrorPolicyisnone. This makes it possible to remove theundefinedchecks or optional chaining in most cases.
- #13130
- 4.1.923 Apr 2026
Release notes3 sources agree
Open source →Patch Changes
- #13203
099954bThanks @copilot-swe-agent! - Remove theworkspacesfield from the publishedpackage.jsonindistto avoid Yarn v1 warnings about workspaces requiring private packages.
- #13203
- 4.1.823 Apr 2026
Release notes3 sources agree
Open source →Patch Changes
- #13202
8a51ea6Thanks @phryneas! - Ship agent skill for usage with @tanstack/intent — the skill is now bundled in the npm package underskills/apollo-client/and discoverable byintent list. For more context, see the TanStack Intent QuickStart.
- #13202
- 4.1.78 Apr 2026
Release notes3 sources agree
Open source →Patch Changes
- #13187
bb3fd9bThanks @jerelmiller! - Fix RxJS interop issue with the observable returned byWebSocketLink.
- #13187
- 4.1.623 Feb 2026
Release notes3 sources agree
Open source →Patch Changes
-
#13128
6c0b8e4Thanks @pavelivanov! - FixuseQueryhydration mismatch whenssr: falseandskip: trueare used togetherWhen both options were combined, the server would return
loading: false(becauseuseSSRQuerychecksskipfirst), but the client'sgetServerSnapshotwas returningssrDisabledResultwithloading: true, causing a hydration mismatch.
-
- 4.1.519 Feb 2026
Release notes3 sources agree
Open source →Patch Changes
-
#13155
3ba1583Thanks @jerelmiller! - Fix an issue whereuseQuerywould poll withpollIntervalwhenskipwas initialized totrue. -
#13135
fd42142Thanks @jerelmiller! - Fix issue whereclient.querywould apply options fromdefaultOptions.watchQuery.
-
- 4.1.45 Feb 2026
- 4.1.328 Jan 2026
Release notes3 sources agree
Open source →Patch Changes
-
#13111
bf46fe0Thanks @RogerHYang! - FixcreateFetchMultipartSubscriptionto support cancellation viaAbortControllerPreviously, calling
dispose()orunsubscribe()on a subscription created bycreateFetchMultipartSubscriptionhad no effect - the underlying fetch request would continue running until completion. This was because noAbortControllerwas created or passed tofetch(), and no cleanup function was returned from the Observable.
-
- 4.1.221 Jan 2026
Release notes3 sources agree
Open source →Patch Changes
-
#13105
8b62263Thanks @phryneas! -ssrMode,ssrForceFetchDelayorprioritizeCacheValuesshould not overridefetchPolicy: 'cache-only',fetchPolicy: 'no-cache',fetchPolicy: 'standby',skip: true, orskipTokenwhen reading the initial value of anObservableQuery. -
#13105
8b62263Thanks @phryneas! - FixskipTokeninuseQuerywithprerenderStaticand related SSR functions. -
#13105
8b62263Thanks @phryneas! - Avoid fetches withfetchPolicy: no-cacheinuseQuerywithprerenderStaticand related SSR functions.
-
- 4.1.120 Jan 2026
Release notes3 sources agree
Open source →Patch Changes
- #13103
dee7dcfThanks @jerelmiller! - Ensure@clientfields that are children of aliased server fields are resolved correctly.
- #13103
- 4.1.015 Jan 2026
Release notes3 sources agree
Open source →Minor Changes
-
#13043
65e66caThanks @jerelmiller! - Supportheaderstransport for enhanced client awareness. -
#12927
785e223Thanks @jerelmiller! - You can now provide a callback function as thecontextoption on themutatefunction returned byuseMutation. The callback function is called with the value of thecontextoption provided to theuseMutationhook. This is useful if you'd like to merge the context object provided to theuseMutationhook with a value provided to themutatefunction.function MyComponent() { const [mutate, result] = useMutation(MUTATION, { context: { foo: true }, }); async function runMutation() { await mutate({ // sends context as { foo: true, bar: true } context: (hookContext) => ({ ...hookContext, bar: true }), }); } // ... } -
#12923
94ea3e3Thanks @jerelmiller! - Fix an issue where deferred payloads that returned arrays with fewer items than the original cached array would retain items from the cached array. This change includes@streamarrays where stream arrays replace the cached arrays. -
#12927
96b531fThanks @jerelmiller! - Don't set the fallback value of a@clientfield tonullwhen areadfunction is defined. Instead thereadfunction will be called with anexistingvalue ofundefinedto allow default arguments to be used to set the returned value.When a
readfunction is not defined nor is there a defined resolver for the field, warn and set the value tonullonly in that instance. -
#12927
45ebb52Thanks @jerelmiller! - Add support forfrom: nullinclient.watchFragmentandcache.watchFragment. Whenfromisnull, the emitted result is:{ data: null, dataState: "complete", complete: true, } -
#12926
2b7f2c1Thanks @jerelmiller! - Support the newer incremental delivery format for the@deferdirective implemented in[email protected]. Import theGraphQL17Alpha9Handlerto use the newer incremental delivery format with@defer.import { GraphQL17Alpha9Handler } from "@apollo/client/incremental"; const client = new ApolloClient({ // ... incrementalHandler: new GraphQL17Alpha9Handler(), });[!NOTE] In order to use the
GraphQL17Alpha9Handler, the GraphQL server MUST implement the newer incremental delivery format. You may see errors or unusual behavior if you use the wrong handler. If you are using Apollo Router, continue to use theDefer20220824Handlerbecause Apollo Router does not yet support the newer incremental delivery format. -
#12927
45ebb52Thanks @jerelmiller! - Add support for arrays withuseFragment,useSuspenseFragment, andclient.watchFragment. This allows the ability to use a fragment to watch multiple entities in the cache. Passing an array tofromwill returndataas an array where each array index corresponds to the index in thefromarray.function MyComponent() { const result = useFragment({ fragment, from: [item1, item2, item3], }); // `data` is an array with 3 items console.log(result); // { data: [{...}, {...}, {...}], dataState: "complete", complete: true } } -
#12927
45ebb52Thanks @jerelmiller! - Add agetCurrentResultfunction to the observable returned byclient.watchFragmentandcache.watchFragmentthat returns the current value for the watched fragment.const observable = client.watchFragment({ fragment, from: { __typename: "Item", id: 1 }, }); console.log(observable.getCurrentResult()); // { // data: {...}, // dataState: "complete", // complete: true, // } -
#13038
109efe7Thanks @jerelmiller! - Add thefromoption toreadFragment,watchFragment, andupdateFragment. -
#12918
2e224b9Thanks @jerelmiller! - Add support for the@streamdirective on both theDefer20220824Handlerand theGraphQL17Alpha2Handler.[!NOTE] The implementations of
@streamdiffer in the delivery of incremental results between the different GraphQL spec versions. If you upgrading from the older format to the newer format, expect the timing of some incremental results to change. -
#13056
b224efcThanks @jerelmiller! -InMemoryCacheno longer filters out explicitly returnedundefineditems fromreadfunctions for array fields. This now makes it possible to createreadfunctions on array fields that return partial data and trigger a fetch for the full list. -
#13058
121a2cbThanks @jerelmiller! - Add anextensionsoption tocache.write,cache.writeQuery, andclient.writeQuery. This makesextensionsavailable in cachemergefunctions which can be accessed with the other merge function options.As a result of this change, any
extensionsreturned in GraphQL operations are now available inmergein the cache writes for these operations. -
#12927
96b531fThanks @jerelmiller! - Add an abstractresolvesClientFieldfunction toApolloCachethat can be used by caches to tellLocalStateif it can resolve a@clientfield when a local resolver is not defined.LocalStatewill emit a warning and set a fallback value ofnullwhen no local resolver is defined andresolvesClientFieldreturnsfalse, or isn't defined. ReturningtruefromresolvesClientFieldsignals that a mechanism in the cache will set the field value. In this case,LocalStatewon't set the field value. -
#13078
bf1e0dcThanks @phryneas! - Use the default stream merge function for@streamfields only if stream info is present. This change means that using the olderDefer20220824Handlerwill not use the default stream merge function and will instead truncate the streamed array on the first chunk.
Patch Changes
-
#12884
d329790Thanks @phryneas! - Ensure thatPreloadedQueryRefinstances are unsubscribed when garbage collected -
#13086
1a1d408Thanks @phryneas! - Change the returned value fromnullto{}when all fields in a query were skipped.This also fixes a bug where
useSuspenseQuerywould suspend indefinitely when all fields were skipped. -
#13010
7627000Thanks @jerelmiller! - Fix an issue where errors parsed from incremental chunks inErrorLinkmight throw when using theGraphQL17Alpha9Handler. -
#12927
45ebb52Thanks @jerelmiller! - Deduplicate watches created byuseFragment,client.watchFragment, andcache.watchFragmentthat contain the same fragment, variables, and identifier. This should improve performance in situations where auseFragmentor aclient.watchFragmentis used to watch the same object in multiple places of an application. -
#12927
259ae9bThanks @jerelmiller! - AllowFragmentTypenot only to be called asFragmentType<TData>, but also asFragmentType<TypedDocumentNode>. -
#12925
5851800Thanks @jerelmiller! - Fix an issue where callingfetchMorewith@deferor@streamwould not rerender incremental results as they were streamed. -
#12927
9e55188Thanks @jerelmiller! - Truncate@streamarrays only on last chunk by default. -
#13083
f3c2be1Thanks @phryneas! - Expose theExtensionsWithStreamInfotype forextensionsinCache.writeQuery,Cache.writeandCache.updateso other cache implementations also can correctly access them. -
#12923
94ea3e3Thanks @jerelmiller! - Improve the cache data loss warning message whenexistingorincomingis an array. -
#12927
4631175Thanks @jerelmiller! - Ignore top-leveldatavalues on subsequent chunks in incremental responses. -
#12927
2be8de2Thanks @jerelmiller! - Create mechanism to add experimental features to Apollo Client -
#12927
96b531fThanks @jerelmiller! - EnsureLocalStatedoesn't try to read from the cache when using ano-cachefetch policy. -
#12927
bb8ed7bThanks @jerelmiller! - Ensure an error is thrown when@streamis detected and anincrementalDeliveryhandler is not configured. -
#13053
23ca0baThanks @phryneas! - Use memoized observable mapping when usingwatchFragment,useFragmentoruseSuspenseFragment. -
#12927
44706a2Thanks @jerelmiller! - Add helper typeQueryRef.ForQuery<TypedDocumentNode> -
#13082
c257418Thanks @phryneas! - PassstreamInfothrough result extensions as aWeakRef. -
#12927
4631175Thanks @jerelmiller! - Fix theDefer20220824Handler.SubsequentResulttype to match theFormattedSubsequentIncrementalExecutionResulttype in[email protected]. -
#12927
96b531fThanks @jerelmiller! - Warn when using ano-cachefetch policy without a local resolver defined.no-cachequeries do not read or write to the cache which meantno-cachequeries are silently incomplete when the@clientfield value was handled by a cachereadfunction. -
#12927
5776ea0Thanks @jerelmiller! - Update theacceptheader used with theGraphQL17Alpha9Handlertomultipart/mixed;incrementalSpec=v0.2to ensure the newest incremental delivery format is requested. -
#12927
45ebb52Thanks @jerelmiller! -DeepPartial<Array<TData>>now returnsArray<DeepPartial<TData>>instead ofArray<DeepPartial<TData | undefined>>. -
#13071
99ffe9aThanks @phryneas! -prerenderStatic: Expose return value ofrenderFunctionto userland, fixabortedproperty.This enables usage of
resumeAndPrerenderwith React 19.2. -
#13026
05eee67Thanks @jerelmiller! - Reduce the number of observables created bywatchFragmentby reusing existing observables as much as possible. This should improve performance when watching the same item in the cache multiple times after a cache update occurs. -
#13010
7627000Thanks @jerelmiller! - Handle@streampayloads that send multiple items in the same chunk when using theDefer20220824Handler. -
#13010
7627000Thanks @jerelmiller! - Handle an edge case with theDefer20220824Handlerwhere an error for a@streamitem that bubbles to the@streamboundary (such as an item returningnullfor a non-null array item) would write items from future chunks to the wrong array index. In these cases, the@streamfield is no longer processed and future updates to the field are ignored. This prevents runtime errors that TypeScript would otherwise not be able to catch. -
#13081
1e06ad7Thanks @jerelmiller! - Avoid callingmergefunctions more than once for the same incremental chunk.
-
- 4.1.0-rc.19 Jan 2026pre-release
Release notes3 sources agree
Open source →Patch Changes
-
#13086
1a1d408Thanks @phryneas! - Change the returned value fromnullto{}when all fields in a query were skipped.This also fixes a bug where
useSuspenseQuerywould suspend indefinitely when all fields were skipped. -
#13071
99ffe9aThanks @phryneas! -prerenderStatic: Expose return value ofrenderFunctionto userland, fixabortedproperty.This enables usage of
resumeAndPrerenderwith React 19.2.
-
- 4.1.0-rc.07 Jan 2026pre-release
Release notes3 sources agree
Open source →Minor Changes
- #13078
bf1e0dcThanks @phryneas! - Use the default stream merge function for@streamfields only if stream info is present. This change means that using the olderDefer20220824Handlerwill not use the default stream merge function and will instead truncate the streamed array on the first chunk.
Patch Changes
-
#13083
f3c2be1Thanks @phryneas! - Expose theExtensionsWithStreamInfotype forextensionsinCache.writeQuery,Cache.writeandCache.updateso other cache implementations also can correctly access them. -
#13082
c257418Thanks @phryneas! - PassstreamInfothrough result extensions as aWeakRef. -
#13081
1e06ad7Thanks @jerelmiller! - Avoid callingmergefunctions more than once for the same incremental chunk.
- #13078
- 4.1.0-alpha.918 Dec 2025pre-release
Release notes3 sources agree
Open source →Minor Changes
-
#13056
b224efcThanks @jerelmiller! -InMemoryCacheno longer filters out explicitly returnedundefineditems fromreadfunctions for array fields. This now makes it possible to createreadfunctions on array fields that return partial data and trigger a fetch for the full list. -
#13058
121a2cbThanks @jerelmiller! - Add anextensionsoption tocache.write,cache.writeQuery, andclient.writeQuery. This makesextensionsavailable in cachemergefunctions which can be accessed with the other merge function options.As a result of this change, any
extensionsreturned in GraphQL operations are now available inmergein the cache writes for these operations.
Patch Changes
-
- 4.1.0-alpha.85 Dec 2025pre-release
Release notes3 sources agree
Open source →Minor Changes
- #13043
65e66caThanks @jerelmiller! - Supportheaderstransport for enhanced client awareness.
- #13043
- 4.1.0-alpha.73 Dec 2025pre-release
Release notes3 sources agree
Open source →Minor Changes
- #13038
109efe7Thanks @jerelmiller! - Add thefromoption toreadFragment,watchFragment, andupdateFragment.
- #13038
- 4.1.0-alpha.61 Dec 2025pre-release
Release notes3 sources agree
Open source →Patch Changes
- #13026
05eee67Thanks @jerelmiller! - Reduce the number of observables created bywatchFragmentby reusing existing observables as much as possible. This should improve performance when watching the same item in the cache multiple times after a cache update occurs.
- #13026
- 4.1.0-alpha.519 Nov 2025pre-release
Release notes3 sources agree
Open source →Patch Changes
-
#13010
7627000Thanks @jerelmiller! - Fix an issue where errors parsed from incremental chunks inErrorLinkmight throw when using theGraphQL17Alpha9Handler. -
#13010
7627000Thanks @jerelmiller! - Handle@streampayloads that send multiple items in the same chunk when using theDefer20220824Handler. -
#13010
7627000Thanks @jerelmiller! - Handle an edge case with theDefer20220824Handlerwhere an error for a@streamitem that bubbles to the@streamboundary (such as an item returningnullfor a non-null array item) would write items from future chunks to the wrong array index. In these cases, the@streamfield is no longer processed and future updates to the field are ignored. This prevents runtime errors that TypeScript would otherwise not be able to catch.
-
- 4.1.0-alpha.417 Nov 2025pre-release
- 4.1.0-alpha.327 Oct 2025pre-release
Release notes3 sources agree
Open source →Minor Changes
-
#12971
d11eb40Thanks @jerelmiller! - Add support forfrom: nullinclient.watchFragmentandcache.watchFragment. Whenfromisnull, the emitted result is:{ data: null, dataState: "complete", complete: true, } -
#12971
d11eb40Thanks @jerelmiller! - Add support for arrays withuseFragment,useSuspenseFragment, andclient.watchFragment. This allows the ability to use a fragment to watch multiple entities in the cache. Passing an array tofromwill returndataas an array where each array index corresponds to the index in thefromarray.function MyComponent() { const result = useFragment({ fragment, from: [item1, item2, item3], }); // `data` is an array with 3 items console.log(result); // { data: [{...}, {...}, {...}], dataState: "complete", complete: true } } -
#12971
d11eb40Thanks @jerelmiller! - Add agetCurrentResultfunction to the observable returned byclient.watchFragmentandcache.watchFragmentthat returns the current value for the watched fragment.const observable = client.watchFragment({ fragment, from: { __typename: "Item", id: 1 }, }); console.log(observable.getCurrentResult()); // { // data: {...}, // dataState: "complete", // complete: true, // }
Patch Changes
-
#12971
d11eb40Thanks @jerelmiller! - Deduplicate watches created byuseFragment,client.watchFragment, andcache.watchFragmentthat contain the same fragment, variables, and identifier. This should improve performance in situations where auseFragmentor aclient.watchFragmentis used to watch the same object in multiple places of an application. -
#12982
5c56b32Thanks @jerelmiller! - Ignore top-leveldatavalues on subsequent chunks in incremental responses. -
#12982
5c56b32Thanks @jerelmiller! - Fix theDefer20220824Handler.SubsequentResulttype to match theFormattedSubsequentIncrementalExecutionResulttype in[email protected]. -
#12973
072da24Thanks @jerelmiller! - Update theacceptheader used with theGraphQL17Alpha9Handlertomultipart/mixed;incrementalSpec=v0.2to ensure the newest incremental delivery format is requested. -
#12971
d11eb40Thanks @jerelmiller! -DeepPartial<Array<TData>>now returnsArray<DeepPartial<TData>>instead ofArray<DeepPartial<TData | undefined>>.
-
- 4.1.0-alpha.210 Oct 2025pre-release
Release notes3 sources agree
Open source →Minor Changes
-
#12959
556e837Thanks @jerelmiller! - You can now provide a callback function as thecontextoption on themutatefunction returned byuseMutation. The callback function is called with the value of thecontextoption provided to theuseMutationhook. This is useful if you'd like to merge the context object provided to theuseMutationhook with a value provided to themutatefunction.function MyComponent() { const [mutate, result] = useMutation(MUTATION, { context: { foo: true }, }); async function runMutation() { await mutate({ // sends context as { foo: true, bar: true } context: (hookContext) => ({ ...hookContext, bar: true }), }); } // ... }
Patch Changes
- #12954
1c82eafThanks @jerelmiller! - Ensure an error is thrown when@streamis detected and anincrementalDeliveryhandler is not configured.
-
- 4.1.0-alpha.126 Sept 2025pre-release
Release notes3 sources agree
Open source →Minor Changes
-
#12934
54ab6d9Thanks @jerelmiller! - Don't set the fallback value of a@clientfield tonullwhen areadfunction is defined. Instead thereadfunction will be called with anexistingvalue ofundefinedto allow default arguments to be used to set the returned value.When a
readfunction is not defined nor is there a defined resolver for the field, warn and set the value tonullonly in that instance. -
#12934
54ab6d9Thanks @jerelmiller! - Add an abstractresolvesClientFieldfunction toApolloCachethat can be used by caches to tellLocalStateif it can resolve a@clientfield when a local resolver is not defined.LocalStatewill emit a warning and set a fallback value ofnullwhen no local resolver is defined andresolvesClientFieldreturnsfalse, or isn't defined. ReturningtruefromresolvesClientFieldsignals that a mechanism in the cache will set the field value. In this case,LocalStatewon't set the field value.
Patch Changes
-
#12915
c97b145Thanks @phryneas! - Create mechanism to add experimental features to Apollo Client -
#12934
54ab6d9Thanks @jerelmiller! - EnsureLocalStatedoesn't try to read from the cache when using ano-cachefetch policy. -
#12934
54ab6d9Thanks @jerelmiller! - Warn when using ano-cachefetch policy without a local resolver defined.no-cachequeries do not read or write to the cache which meantno-cachequeries are silently incomplete when the@clientfield value was handled by a cachereadfunction.
-
- 4.1.0-alpha.017 Sept 2025pre-release
Release notes3 sources agree
Open source →Minor Changes
-
#12923
2aa31c7Thanks @jerelmiller! - Fix an issue where deferred payloads that reteurned arrays with fewer items than the original cached array would retain items from the cached array. This change includes@streamarrays where stream arrays replace the cached arrays. -
#12926
c7fba99Thanks @jerelmiller! - Support the newer incremental delivery format for the@deferdirective implemented in[email protected]. Import theGraphQL17Alpha9Handlerto use the newer incremental delivery format with@defer.import { GraphQL17Alpha9Handler } from "@apollo/client/incremental"; const client = new ApolloClient({ // ... incrementalHandler: new GraphQL17Alpha9Handler(), });[!NOTE] In order to use the
GraphQL17Alpha9Handler, the GraphQL server MUST implement the newer incremental delivery format. You may see errors or unusual behavior if you use the wrong handler. If you are using Apollo Router, continue to use theDefer20220824Handlerbecause Apollo Router does not yet support the newer incremental delivery format. -
#12918
562e219Thanks @jerelmiller! - Add support for the@streamdirective on both theDefer20220824Handlerand theGraphQL17Alpha2Handler.[!NOTE] The implementations of
@streamdiffer in the delivery of incremental results between the different GraphQL spec versions. If you upgrading from the older format to the newer format, expect the timing of some incremental results to change.
Patch Changes
-
#12925
f538a83Thanks @jerelmiller! - Fix an issue where callingfetchMorewith@deferor@streamwould not rerender incremental results as they were streamed. -
#12923
01cace0Thanks @jerelmiller! - Improve the cache data loss warning message whenexistingorincomingis an array.
-
- 4.0.1313 Jan 2026
Release notes3 sources agree
Open source →Patch Changes
-
#13094
9cbe2c2Thanks @phryneas! - Ensure thatcompactandmergeOptionspreserve symbol keys.This fixes an issue where the change introduced in 4.0.11 via #13049 would not be applied if
defaultOptionsforwatchQuerywere declared.Please note that
compactandmergeOptionsare considered internal utilities and they might have similar behavior changes in future releases. Do not use them in your application code - a change like this is not considered breaking and will not be announced as such.
-
- 4.0.1212 Jan 2026