@apollo/client
4.3.5 · 19 Oct 2026MIT5.3M / weekRepositoryHomepagenpmAtom feed
4.3.5 is the newest release of 4.x.
In short
1 removed1 deprecated18 new17 fixed
- removed
Remove the optional modifier from the
variablesproperty provided to theupdatefunction inclient.mutateanduseMutation.variablesis always a defined object, even when variables are not provided to the mutation. - deprecated
Mark
skipas deprecated inuseQueryanduseSubscriptionnow that both of these hooks supportskipToken.
Release note
Minor Changes
Field policies and
inputObjectscan now tell the cache whether a field is a list of scalars or a scalar whose value is an array. Previously all arrays were iterated and only the inner type was provided to the scalarparse/serializefunctions.This required some breaking changes from previous prerelease versions:
The field policy
scalaroption andinputObjectstype string now use GraphQL list syntax to mark a field as a list of scalarsThe abstract
cache.getScalarForFieldis nowcache.getScalarTypeForFieldand is expected to return the string representing the scalar type rather than theScalarinstance
new InMemoryCache({ scalars: { DateTime: new Scalar(/*...*/), }, inputObjects: { EventFilter: { fields: { // Previously only the scalar type was provided datesBefore: "DateTime", // List syntax now required datesAfter: "[DateTime]", dates2d: "[[DateTime]]", }, }, }, typePolicies: { Event: { fields: { // Previously only the scalar type was provided datesBefore: { scalar: "DateTime", }, // List syntax now required datesAfter: { scalar: "[DateTime]", }, dates2d: { scalar: "[[DateTime]]", }, }, }, }, });Now it's possible to handle scalars that are represented by arrays:
const dateTimeRangeScalar = new Scalar< [string, string], { start: Date; end: Date } >({ parse: ([start, end]) => ({ start: new Date(start), end: new Date(end), }), serialize: (range) => [range.start.toISOString(), range.end.toISOString()], is: (value) => !Array.isArray(value), }); const cache = new InMemoryCache({ scalars: { DateTimeRange: dateTimeRangeScalar, }, typePolicies: { Event: { fields: { range: { scalar: "DateTimeRange", }, }, }, }, }); const query = gql` query { event { range } } `; cache.writeQuery({ query, data: { event: { __typename: "Event", // Server returns DateTimeRange as a JSON array range: ["2024-01-01T00:00:00Z", "2024-06-01T00:00:00Z"], }, }, }); const { data } = useQuery(query); // => { event: { __typename: "Event", range: { start: Date, end: Date } } }Fix the accuracy of
dataStatein 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: "john@example.com", }, }, };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: "john@example.com", }, }, };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.Adds
Scalar.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, });The minimum supported TypeScript version is now 5.9.x.
Adds the plumbing and types implementation for declaring custom scalars and configuring custom scalars in
InMemoryCache.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, }), }, });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.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 a
mergefunction to a field policy.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.Version bump only to
rc.Add
GraphQLCodegenIncrementaltype overrides that assemble GraphQL Codegen@deferoperation types whendataStateis"complete".Parse scalar fields for
no-cachequeries.Support
skipTokenwithuseSubscriptionto 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 );Allow overriding the
frominput 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; } }Field policy
readandmergefunctions are now ignored when the field policy configures thescalaroption. If areadormergefunction is provided alongsidescalar, a development-only warning is emitted.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: "user.cache@example.com" }} // 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: "user.cache@example.com" }, dataState: "complete" } // 3. email updated from the server { data: { user: 1, name: "User", email: "user.network@example.com" }, 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: "user.network@example.com" }, 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.Adds a
scalaroption 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().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" } }Remove the custom
NoInfertype utility in favor of the nativeNoInferintroduced in TypeScript 5.4.Adds the
getScalarabstract method toApolloCachethat cache subclasses override to provide scalar behavior to Apollo Client. Defaults to unconditionally returnundefinedif not specified.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.
Make sure scalars are correctly returned for mutations and subscriptions.
Note
This change affects cache reads after mutations as mutations now reread from the cache after each execution that writes a result to the cache. Previously mutations only reread from the cache when an update function was provided. This can have an effect on the result if you have union or interface types in the mutation but forget to include possibleTypes which might return a result that doesn't include the interface/union fields.
Patch Changes
Fix
dataStateto report"streaming"instead of"partial"whenreturnPartialDataistrueand the cache result is missing only@deferfields.Fix an issue where a
network-onlyquery leaked partial cache data for@deferfragments that were not delivered by the network due to an error that bubbled to the@deferfragment boundary.Fix an issue where a sibling non-deferred fragment might be accidentally pruned when the
@deferfragment hadn't been delivered.- removed
Remove the optional modifier from the
variablesproperty provided to theupdatefunction inclient.mutateanduseMutation.variablesis always a defined object, even when variables are not provided to the mutation. Fix an issue where field
readfunctions 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" } }Fix issue where the wrong
dataStatewas returned when there was nothing written to the cache and a@deferfragment was marked pending.Fix an issue where
network-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.Cache diffs for incomplete queries no longer pay the cost of building a full
MissingFieldErrorwhen 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.Fix an issue where a
@deferquery reported thedataStateascompleteinstead ofstreamingwhen an error occurs on a deferred field that bubbled to the defer boundary.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.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.
- deprecated
Mark
skipas deprecated inuseQueryanduseSubscriptionnow that both of these hooks supportskipToken. Fix issue where setting
returnPartialData: truemight report the wrongdataStatewhen partial data was written to the cache and@deferfragments were pending.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.Fix an invariant error thrown when a
@deferboundary received a payload after it had already been marked complete.Align the remaining cache generic constraints with
Cache.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.