NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
npm · #4032 most downloaded on npm
A fully-featured caching GraphQL client.
Last release 16 days ago
18 Sep 2026
Ships fairly regularly
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
727 releases · first in 2019
Patch Changes #13464 37f700e Thanks @jerelmiller ! - Fix an issue where useLazyQuery did not rerender with new variables until the network request had
37f700e Thanks @jerelmiller! - Fix an issue where useLazyQuery did not rerender with new variables until the network request had completed when calling execute with new variables while a request was already in-flight.This required some breaking changes from previous prerelease versions:
#13447 24133fe Thanks @jerelmiller! - Field policies and inputObjects can 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 scalar parse/serialize functions.
This required some breaking changes from previous prerelease versions:
scalar option and inputObjects type string now use GraphQL list syntax to mark a field as a list of scalarscache.getScalarForField is now cache.getScalarTypeForField and is expected to return the string representing the scalar type rather than the Scalar instancenew 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 } } }#13324 0abd8de Thanks @jerelmiller! - Fix the accuracy of dataState in complex incremental streaming scenarios, especially when combined with returnPartialData: true.
Prior to this change, all intermediate chunks used for both @defer and @stream directives returned a dataState of streaming, regardless of whether the actual data shape fit the definition of the streaming data state. The streaming data state represents an incomplete incremental response where the only holes in the data occur at @defer boundaries.
Let's use the following example of where the previous dataState fell down when combined with returnPartialData.
query GreetingQuery {
greeting {
message
... @defer {
recipient {
name
email
}
}
}
}@defer boundary written to the cacheLet'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 complete because recipient.email is missing. This data is also not streaming because the data requirements in the @defer boundary are partially fulfilled due to the existence of recipient. This could lead to runtime crashes on recipient.email if you use the existence of recipient to detect whether data in the @defer boundary has streamed in or not. This change now accurately reports this as partial to ensure the field is marked as a partial field in recipient.
@defer boundaryLet'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 as dataState: "complete" since it is safe to access data on all fields.
This change also means @stream queries by definition fulfill the data requirements of the query after the first chunk arrives since @stream operates on lists and contains no data holes. @stream queries now accurately report dataState as complete or partial, 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 dataState reported as "streaming" are now reported as partial or complete.
If you use dataState to determine whether an incremental request is still in-flight, please use networkStatus instead to check for NetworkStatus.streaming. dataState is type narrowing feature and not intended to report the network status.
#13274 7b10078 Thanks @jerelmiller! - Adds Scalar.fromGraphQLScalarType helper to create a Scalar instance from an existing graphql.js GraphQLScalarType.
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,
});#13421 d6197a4 Thanks @jerelmiller! - The minimum supported TypeScript version is now 5.9.x.
#13270 d080f11 Thanks @jerelmiller! - 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.Scalars interface:
// apollo.d.ts
import "@apollo/client";
declare module "@apollo/client" {
namespace ApolloCache {
interface Scalars {
Date: { serialized: string; parsed: Date };
}
}
}This enables the scalars option in InMemoryCache:
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,
}),
},
});#13250 bad7035 Thanks @jerelmiller! - Add the ability to define the cache type for the client. client.cache currently returns ApolloCache as the cache type regardless of what cache you've provided to ApolloClient.
Declare the cache type using the cache property in the TypeOverrides interface 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 cache is 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 thecacheoption for theApolloClientconstructor.
#13406 bd74ccb Thanks @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 a merge function to a field policy.
#13390 90e338c Thanks @jerelmiller! - Fix issue where sibling @defer fragments were pruned incorrectly when at least one of the @defer fragments wasn't delivered.
As a result of this change, a label argument is now added to all outgoing @defer directives when using the GraphQL17Alpha9Handler in order to disambiguate the @defer fragments from each other.
#13426 a9beaff Thanks @jerelmiller! - Version bump only to rc.
#13416 f2d5d5a Thanks @jerelmiller! - Add GraphQLCodegenIncremental type overrides that assemble GraphQL Codegen @defer operation types when dataState is "complete".
#13372 e4cde69 Thanks @jerelmiller! - Parse scalar fields for no-cache queries.
#13386 0be8fd8 Thanks @atharv-sys32! - Support skipToken with useSubscription to 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
);#13337 2df711f Thanks @jcostello-atlassian! - Allow overriding the from input of useFragment, useSuspenseFragment, readFragment, writeFragment and related fragment APIs via a new FromOptionValue key on the TypeOverrides interface.
By default, from continues to accept StoreObject | Reference | FragmentType<TData> | string. Apps can now supply a stricter policy (for example, requiring __typename and disallowing nullish identifier values) without affecting StoreObject, cache.identify, cache.modify or 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;
}
}#13405 f923ab4 Thanks @jerelmiller! - Field policy read and merge functions are now ignored when the field policy configures the scalar option. If a read or merge function is provided alongside scalar, a development-only warning is emitted.
#13393 434d25f Thanks @jerelmiller! - Change when @defer fragments and @stream fields are pruned for cache-first and cache-and-network fetch policies to better match the network when the initial value contained a partial result:
cache-first: prune undelivered @defer fragments or @stream items when the result is fetched from the network due to a partial resultcache-and-network: prune undelivered @defer fragments or @stream items 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-first fetch policy where all @defer fields 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 contain email).
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 @defer boundaries 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.
#13270 6031987 Thanks @jerelmiller! - Adds a scalar option to InMemoryCache field 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() and cache.restore().
#13273 0886de1 Thanks @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 inputObjects option is available to InMemoryCache that 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" } }#13424 d2bca2e Thanks @jerelmiller! - Remove the custom NoInfer type utility in favor of the native NoInfer introduced in TypeScript 5.4.
#13270 d080f11 Thanks @jerelmiller! - Adds the getScalar abstract method to ApolloCache that cache subclasses override to provide scalar behavior to Apollo Client. Defaults to unconditionally return undefined if not specified.
#13406 bd74ccb Thanks @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 read functions 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.
#13408 7a5164d Thanks @jerelmiller! - Fix dataState to report "streaming" instead of "partial" when returnPartialData is true and the cache result is missing only @defer fields.
#13381 9c73762 Thanks @jerelmiller! - Fix an issue where a network-only query leaked partial cache data for @defer fragments that were not delivered by the network due to an error that bubbled to the @defer fragment boundary.
#13390 90e338c Thanks @jerelmiller! - Fix an issue where a sibling non-deferred fragment might be accidentally pruned when the @defer fragment hadn't been delivered.
#13442 ed033d4 Thanks @jerelmiller! - Remove the optional modifier from the variables property provided to the update function in client.mutate and useMutation. variables is always a defined object, even when variables are not provided to the mutation.
#13324 0abd8de Thanks @jerelmiller! - Fix an issue where field read functions were not applied to intermediate results while streaming @defer responses. cache.diff ran the read functions, 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 @defer boundaries now correctly return the result of field read functions.
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" } }#13403 aaff7a8 Thanks @jerelmiller! - Fix issue where the wrong dataState was returned when there was nothing written to the cache and a @defer fragment was marked pending.
#13347 7d543d6 Thanks @jerelmiller! - Fix an issue where network-only incremental queries could cause cache data to leak into the emitted result when a @defer or @stream boundary already had complete data in the cache. Cache data inside pending @defer objects and @stream arrays are now pruned so that only completed @defer or @stream boundaries are returned.
NOTE: This change only applies to InMemoryCache when using GraphQL17Alpha9Handler.
#13329 1d581d2 Thanks @AmariahAK! - Cache diffs for incomplete queries no longer pay the cost of building a full MissingFieldError when the missing property is not accessed. The error object is now only constructed when the missing property is accessed the first time. This improves performance by avoiding a V8 stack capture when missing is ignored entirely.
As an additional small performance improvement, JSON.stringify is no longer used in the error message on objects whose cache ID is known. JSON.stringify is only used for non-normalized objects.
#13381 9c73762 Thanks @jerelmiller! - Fix an issue where a @defer query reported the dataState as complete instead of streaming when an error occurs on a deferred field that bubbled to the defer boundary.
#13324 0abd8de Thanks @jerelmiller! - Fix an issue with @stream queries when using returnPartialData: true where 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. The dataState is now reported as partial until the server has streamed enough of the list so that each list item fully satisfies the query.
This change also updates @stream queries so that they reported with dataState: "complete instead of "streaming" since it is safe to access all fields in the response.
#13373 2551937 Thanks @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.
#13448 77e1e35 Thanks @jerelmiller! - Mark skip as deprecated in useQuery and useSubscription now that both of these hooks support skipToken.
#13403 aaff7a8 Thanks @jerelmiller! - Fix issue where setting returnPartialData: true might report the wrong dataState when partial data was written to the cache and @defer fragments were pending.
#13347 7d543d6 Thanks @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 @defer data had streamed in or not.
#13381 9c73762 Thanks @jerelmiller! - Fix an invariant error thrown when a @defer boundary received a payload after it had already been marked complete.
#13268 419e2b5 Thanks @DaleSeo! - Align the remaining cache generic constraints with Cache.Implementation. The deprecated React mutation types (MutationHookOptions, MutationFunctionOptions, MutationTuple) and the internal InternalRefetchQueriesOptions and QueryInfo types still constrained their cache type parameter to ApolloCache, so they now match the rest of the overridable cache API.
One column per quarter.
Patch Changes #13448 77e1e35 Thanks @jerelmiller ! - Mark skip as deprecated in useQuery and useSubscription now that both of these hooks support skip
77e1e35 Thanks @jerelmiller! - Mark skip as deprecated in useQuery and useSubscription now that both of these hooks support skipToken.This required some breaking changes from previous prerelease versions:
#13447 24133fe Thanks @jerelmiller! - Field policies and inputObjects can 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 scalar parse/serialize functions.
This required some breaking changes from previous prerelease versions:
scalar option and inputObjects type string now use GraphQL list syntax to mark a field as a list of scalarscache.getScalarForField is now cache.getScalarTypeForField and is expected to return the string representing the scalar type rather than the Scalar instancenew 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 } } }ed033d4 Thanks @jerelmiller! - Remove the optional modifier from the variables property provided to the update function in client.mutate and useMutation. variables is always a defined object, even when variables are not provided to the mutation.Minor Changes #13426 a9beaff Thanks @jerelmiller ! - Version bump only to rc .
a9beaff Thanks @jerelmiller! - Version bump only to rc.<div class="highlight highlight-source-ts notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="import { skipToken, useSub
#13386 0be8fd8 Thanks @atharv-sys32! - Support skipToken with useSubscription to 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 d2bca2e Thanks @jerelmiller! - Remove the custom NoInfer type utility in favor of the native NoInfer introduced in TypeScript 5.4.
By default, from continues to accept StoreObject | Reference | FragmentType<TData> | string . Apps can now supply a stricter policy (for example, requ
#13421 d6197a4 Thanks @jerelmiller! - The minimum supported TypeScript version is now 5.9.x.
#13337 2df711f Thanks @jcostello-atlassian! - Allow overriding the from input of useFragment, useSuspenseFragment, readFragment, writeFragment and related fragment APIs via a new FromOptionValue key on the TypeOverrides interface.
By default, from continues to accept StoreObject | Reference | FragmentType<TData> | string. Apps can now supply a stricter policy (for example, requiring __typename and disallowing nullish identifier values) without affecting StoreObject, cache.identify, cache.modify or 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;
}
}Minor Changes #13416 f2d5d5a Thanks @jerelmiller ! - Add GraphQLCodegenIncremental type overrides that assemble GraphQL Codegen @defer operation types
f2d5d5a Thanks @jerelmiller! - Add GraphQLCodegenIncremental type overrides that assemble GraphQL Codegen @defer operation types when dataState is "complete".Minor Changes #13372 e4cde69 Thanks @jerelmiller ! - Parse scalar fields for no-cache queries.
e4cde69 Thanks @jerelmiller! - Parse scalar fields for no-cache queries.Apollo Client now always writes network results to the cache before delivering them, ensuring custom scalars and field read functions are applied. To
#13406 bd74ccb Thanks @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 a merge function to a field policy.
#13406 bd74ccb Thanks @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 read functions 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.
Minor Changes #13405 f923ab4 Thanks @jerelmiller ! - Field policy read and merge functions are now ignored when the field policy configures the scalar
f923ab4 Thanks @jerelmiller! - Field policy read and merge functions are now ignored when the field policy configures the scalar option. If a read or merge function is provided alongside scalar, a development-only warning is emitted.7a5164d Thanks @jerelmiller! - Fix dataState to report "streaming" instead of "partial" when returnPartialData is true and the cache result is missing only @defer fields.As a result of this change, a label argument is now added to all outgoing @defer directives when using the GraphQL17Alpha9Handler in order to disambig
#13390 90e338c Thanks @jerelmiller! - Fix issue where sibling @defer fragments were pruned incorrectly when at least one of the @defer fragments wasn't delivered.
As a result of this change, a label argument is now added to all outgoing @defer directives when using the GraphQL17Alpha9Handler in order to disambiguate the @defer fragments from each other.
#13393 434d25f Thanks @jerelmiller! - Change when @defer fragments and @stream fields are pruned for cache-first and cache-and-network fetch policies to better match the network when the initial value contained a partial result:
cache-first: prune undelivered @defer fragments or @stream items when the result is fetched from the network due to a partial resultcache-and-network: prune undelivered @defer fragments or @stream items 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-first fetch policy where all @defer fields 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 contain email).
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 @defer boundaries 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.
#13381 9c73762 Thanks @jerelmiller! - Fix an issue where a network-only query leaked partial cache data for @defer fragments that were not delivered by the network due to an error that bubbled to the @defer fragment boundary.
#13390 90e338c Thanks @jerelmiller! - Fix an issue where a sibling non-deferred fragment might be accidentally pruned when the @defer fragment hadn't been delivered.
#13403 aaff7a8 Thanks @jerelmiller! - Fix issue where the wrong dataState was returned when there was nothing written to the cache and a @defer fragment was marked pending.
#13381 9c73762 Thanks @jerelmiller! - Fix an issue where a @defer query reported the dataState as complete instead of streaming when an error occurs on a deferred field that bubbled to the defer boundary.
#13373 2551937 Thanks @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 aaff7a8 Thanks @jerelmiller! - Fix issue where setting returnPartialData: true might report the wrong dataState when partial data was written to the cache and @defer fragments were pending.
#13381 9c73762 Thanks @jerelmiller! - Fix an invariant error thrown when a @defer boundary received a payload after it had already been marked complete.
NOTE: This change only applies to InMemoryCache when using GraphQL17Alpha9Handler.
#13347 7d543d6 Thanks @jerelmiller! - Fix an issue where network-only incremental queries could cause cache data to leak into the emitted result when a @defer or @stream boundary already had complete data in the cache. Cache data inside pending @defer objects and @stream arrays are now pruned so that only completed @defer or @stream boundaries are returned.
NOTE: This change only applies to InMemoryCache when using GraphQL17Alpha9Handler.
#13329 1d581d2 Thanks @AmariahAK! - Cache diffs for incomplete queries no longer pay the cost of building a full MissingFieldError when the missing property is not accessed. The error object is now only constructed when the missing property is accessed the first time. This improves performance by avoiding a V8 stack capture when missing is ignored entirely.
As an additional small performance improvement, JSON.stringify is no longer used in the error message on objects whose cache ID is known. JSON.stringify is only used for non-normalized objects.
#13347 7d543d6 Thanks @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 @defer data had streamed in or not.
Prior to this change, all intermediate chunks used for both @defer and @stream directives returned a dataState of streaming, regardless of whether the
#13324 0abd8de Thanks @jerelmiller! - Fix the accuracy of dataState in complex incremental streaming scenarios, especially when combined with returnPartialData: true.
Prior to this change, all intermediate chunks used for both @defer and @stream directives returned a dataState of streaming, regardless of whether the actual data shape fit the definition of the streaming data state. The streaming data state represents an incomplete incremental response where the only holes in the data occur at @defer boundaries.
Let's use the following example of where the previous dataState fell down when combined with returnPartialData.
query GreetingQuery {
greeting {
message
... @defer {
recipient {
name
email
}
}
}
}
@defer boundary written to the cacheLet'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 complete because recipient.email is missing. This data is also not streaming because the data requirements in the @defer boundary are partially fulfilled due to the existence of recipient. This could lead to runtime crashes on recipient.email if you use the existence of recipient to detect whether data in the @defer boundary has streamed in or not. This change now accurately reports this as partial to ensure the field is marked as a partial field in recipient.
@defer boundaryLet'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 as dataState: "complete" since it is safe to access data on all fields.
This change also means @stream queries by definition fulfill the data requirements of the query after the first chunk arrives since @stream operates on lists and contains no data holes. @stream queries now accurately report dataState as complete or partial, 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 dataState reported as "streaming" are now reported as partial or complete.
If you use dataState to determine whether an incremental request is still in-flight, please use networkStatus instead to check for NetworkStatus.streaming. dataState is type narrowing feature and not intended to report the network status.
#13324 0abd8de Thanks @jerelmiller! - Fix an issue where field read functions were not applied to intermediate results while streaming @defer responses. cache.diff ran the read functions, 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 @defer boundaries now correctly return the result of field read functions.
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 0abd8de Thanks @jerelmiller! - Fix an issue with @stream queries when using returnPartialData: true where 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. The dataState is now reported as partial until the server has streamed enough of the list so that each list item fully satisfies the query.
This change also updates @stream queries so that they reported with dataState: "complete instead of "streaming" since it is safe to access all fields in the response.
```ts import { GraphQLScalarType } from "graphql"; import { Scalar } from "@apollo/client";
#13274 7b10078 Thanks @jerelmiller! - Adds Scalar.fromGraphQLScalarType helper to create a Scalar instance from an existing graphql.js GraphQLScalarType.
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 ed86234 Thanks @jerelmiller! - 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.Scalars interface:
// apollo.d.ts
import "@apollo/client";
declare module "@apollo/client" {
namespace ApolloCache {
interface Scalars {
Date: { serialized: string; parsed: Date };
}
}
}
This enables the scalars option in InMemoryCache:
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 ccaf686 Thanks @jerelmiller! - Adds a scalar option to InMemoryCache field 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() and cache.restore().
#13273 0886de1 Thanks @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 inputObjects option is available to InMemoryCache that 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 ed86234 Thanks @jerelmiller! - Adds the getScalar abstract method to ApolloCache that cache subclasses override to provide scalar behavior to Apollo Client. Defaults to unconditionally return undefined if not specified.
### Patch Changes - #13268 `419e2b5` Thanks @DaleSeo! - Align the remaining cache generic constraints with Cache.Implementation. The deprecated React
419e2b5 Thanks @DaleSeo! - Align the remaining cache generic constraints with Cache.Implementation. The deprecated React mutation types (MutationHookOptions, MutationFunctionOptions, MutationTuple) and the internal InternalRefetchQueriesOptions and QueryInfo types still constrained their cache type parameter to ApolloCache, so they now match the rest of the overridable cache API.Declare the cache type using the cache property in the TypeOverrides interface to set the cache implementation used for the client.
#13250 bad7035 Thanks @jerelmiller! - Add the ability to define the cache type for the client. client.cache currently returns ApolloCache as the cache type regardless of what cache you've provided to ApolloClient.
Declare the cache type using the cache property in the TypeOverrides interface 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 cache is 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.
Patch Changes #13400 56ca81b Thanks @QiRaining ! - Preserve multi-byte UTF-8 characters split across multipart response chunks.
56ca81b Thanks @QiRaining! - Preserve multi-byte UTF-8 characters split across multipart response chunks.### Patch Changes - #13398 `3dd3e9a` Thanks @phryneas! - Fix type signature of some DocumentationTypes to fix their display in our documentation. - #1
#13398 3dd3e9a Thanks @phryneas! - Fix type signature of some DocumentationTypes to fix their display in our documentation.
#13392 d4f0771 Thanks @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 a merge or read function 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.
### Patch Changes - #13385 `bfb674e` Thanks @jerelmiller! - Fix accidental widening of the client.mutate return type when optimisticResponse was prese
#13385 bfb674e Thanks @jerelmiller! - Fix accidental widening of the client.mutate return type when optimisticResponse was present.
#13382 365373e Thanks @jerelmiller! - Fix result types widened when a query's variables had constant types (e.g. TypedDocumentNode<Data, { type: "main" }>). This caused options such as returnPartialData or errorPolicy to be reported as their widened types (e.g. boolean, ErrorPolicy) instead of the value that was passed which returned the wrong data and dataState types.
#13382 365373e Thanks @jerelmiller! - Fix issue where unknown options were permitted by TypeScript when passed alongside a valid option to APIs with modern signatures.
#13383 5840f50 Thanks @jerelmiller! - Update the return type of refetch, fetchMore and useLazyQuery's execute function on the provided errorPolicy. Previously these APIs all used the default type which typed data as TData | undefined and error as ErrorLike | undefined.
### Patch Changes - #13364 `2f383e7` Thanks @atharv-sys32! - Fix a bug where GraphQL variable default values were not applied during cache reads when
#13364 2f383e7 Thanks @atharv-sys32! - Fix a bug where GraphQL variable default values were not applied during cache reads when variables with defaults were explicitly set to undefined. This caused @include/@skip directives to throw "Invalid variable referenced" errors when the variable was passed as undefined instead of being omitted entirely.
#13367 2b39cc8 Thanks @jerelmiller! - Fix an issue where some @export queries would not react to cache updates when the fields keyed by exported variables were updated.
### Patch Changes - #13349 `501a33b` Thanks @jerelmiller! - Prevent the setTimeout in connectToDevtools that shows the devtools suggestion from firing
501a33b Thanks @jerelmiller! - Prevent the setTimeout in connectToDevtools that shows the devtools suggestion from firing when the user agent does not match Chrome or Firefox. This check was previously done inside the setTimeout which meant the timer was scheduled for environments where we'd never show the message anyways. For test environments, this could cause flaky tests when that setTimeout outlived the tests and ran after any virtual DOM was torn down and removed.### Patch Changes - #13320 `538c906` Thanks @jerelmiller! - Cleanup some unused internals. Please file an issue if you notice anything change.
538c906 Thanks @jerelmiller! - Cleanup some unused internals. Please file an issue if you notice anything change.### Patch Changes - #13315 `a406cc9` Thanks @fallintoplace! - Prevent relay multipart subscriptions from issuing a fetch request after serializing the
#13315 a406cc9 Thanks @fallintoplace! - Prevent relay multipart subscriptions from issuing a fetch request after serializing the request body fails.
#13307 abd0781 Thanks @wolfie! - Speed up cache writes by avoiding a full AST visit of 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 @stream on a nested field are no longer treated as streamed themselves and now overwrite existing lists like regular fields instead of merging chunk-wise.
### Patch Changes - #13302 `bb75dd3` Thanks @tpict! - Export KeyArgsFunction and RelayFieldPolicy types from public entrypoints.
### Patch Changes - #13281 `e4df809` Thanks @jerelmiller! - Fixes an issue where client.readFragment and client.readQuery ignored the optimistic optio
e4df809 Thanks @jerelmiller! - Fixes an issue where client.readFragment and client.readQuery ignored the optimistic option when passed in the options object.### Patch Changes - #13254 `66e9dfc` Thanks @jerelmiller! - Add support for graphql v17 as a valid peer dependency.
66e9dfc Thanks @jerelmiller! - Add support for graphql v17 as a valid peer dependency.### Patch Changes - #13184 `c207b88` Thanks @audrius-savickas! - Preserve referential equality of masked data on refetch when the result is deeply equ
c207b88 Thanks @audrius-savickas! - Preserve referential equality of masked data on refetch when the result is deeply equal to the previous result.### Patch Changes - #13248 `062ffe3` Thanks @jerelmiller! - Fixes an issue where useLazyQuery would not apply a changed pollInterval between renders.
062ffe3 Thanks @jerelmiller! - Fixes an issue where useLazyQuery would not apply a changed pollInterval between renders.Apollo Client 4.2 introduces two signature styles for methods and hooks. All signatures previously present are now "classic" signatures, and a new set
#13132 f3ce805 Thanks @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 using TypedDocumentNode to automatically infer types, which provides more accurate results without any manual annotations.
Modern signatures automatically incorporate your declared defaultOptions into 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 global defaultOptions:
// apollo.d.ts
import "@apollo/client";
declare module "@apollo/client" {
export interface TypeOverrides {
signatureStyle: "modern";
}
}
Users can do a global DeclareDefaultOptions type 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 the defaultOptions you've declared.
#13130 dd12231 Thanks @jerelmiller! - Improve the accuracy of client.query return type to better detect the current errorPolicy. The data property is no longer nullable when the errorPolicy is none. This makes it possible to remove the undefined checks or optional chaining in most cases.
#13210 1f9a428 Thanks @jerelmiller! - Add support for automatic event-based refetching, such as window focus.
The RefetchEventManager class handles automatic refetches in response to events. Apollo Client provides built-in sources for window focus and network reconnect as windowFocusSource and onlineSource.
Event refetching is fully opt-in. Create and pass a RefetchEventManager instance to the ApolloClient constructor 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.refetchOn to false and 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.refetchOn and per-query refetchOn options are provided, the objects are merged together.
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 } });
Refetches can be triggered imperatively by calling emit with the event name and its payload (if any).
refetchEventManager.emit("reactNativeAppStatus", "active");
A source that has no automatic detection logic but still wants imperative emit support can be declared as true. Type the event as void to omit the payload argument.
declare module "@apollo/client" {
interface RefetchEvents {
userTriggered: void;
}
}
const refetchEventManager = new RefetchEventManager({
sources: { userTriggered: true },
});
refetchEventManager.emit("userTriggered");
Note: Calling emit on an event without a registered source will log a warning and result in a no-op.
When an event fires, the default handler calls client.refetchQueries({ include: "active" }) filtered by each query's refetchOn setting. You can override the handler for an event to add your own custom filtering. For example, to refetch all queries, including standby queries, 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 RefetchQueriesResult or void. Returning void skips refetching for the event.
#13232 f1b541f Thanks @jerelmiller! - Version bump to rc.
#13206 08fccab Thanks @jerelmiller! - Extend the defaultOptions type-safety work to client.mutate and useMutation.
The errorPolicy option now flows through to the result types for mutations in the same way it already does for queries:
ApolloClient.MutateResult<TData, TErrorPolicy> maps errorPolicy to the concrete shape of data and error:
"none" → { data: TData; error?: never }"all" → { data: TData | undefined; error?: ErrorLike }"ignore" → { data: TData | undefined; error?: never }client.mutate and useMutation pick up the declared defaultOptions.mutate.errorPolicy and the explicit errorPolicy on each call to narrow return types accordingly.useMutation.Result.error is narrowed to undefined when errorPolicy is "ignore", since client.mutate never resolves with an error in that case.DeclareDefaultOptions.Mutate already accepted errorPolicy; 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 | undefined
Setting errorPolicy on an individual call overrides the default for that call's return type.
#13222 b93c172 Thanks @jerelmiller! - Extend the defaultOptions type-safety work to preloadQuery (returned from createQueryPreloader). Defaults declared in DeclareDefaultOptions.WatchQuery now work with preloadQuery to ensure the PreloadedQueryRef'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 f3ce805 Thanks @phryneas! - Synchronize method and hook return types with defaultOptions.
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 | undefined
While these types are generally correct, if you were to set errorPolicy: 'all' as a default option, the type of result.data for the first query would remain TData instead of changing to TData | undefined to match the runtime behavior.
We are now enforcing that certain defaultOptions types need to be registered globally. This means that if you want to use errorPolicy: '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.data in the above example will correctly be changed to TData | undefined, reflecting the possibility that if an error occurs, data might be undefined. Manually specifying useSuspenseQuery(MY_QUERY, { errorPolicy: "none" }); changes result.data to TData to reflect the local override.
This change means that you will need to declare your default options types in order to use defaultOptions with ApolloClient, 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 defaultOptions value 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 in defaultOptions) 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 ApolloClient constructor accepts any of those values in defaultOptions. The tradeoff is that hook and method return types become more generic. For example, calling useSuspenseQuery without an explicit errorPolicy will 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. So errorPolicy?: "all" | "ignore" has the same effect on return types as errorPolicy: "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", declare errorPolicy: "all" | "ignore" (required) to keep the union narrow and avoid unused values broadening your signatures unnecessarily.
#13217 790f987 Thanks @jerelmiller! - Fix the deprecation for the classic signatures for function overloads that rely on type inference from a TypedDocumentNode. The deprecation now only applies to classic signatures that provide explicit type arguments to encourage the use of TypedDocumentNode.
#13166 0537d97 Thanks @jerelmiller! - Release changes in 4.1.5 and 4.1.6.
#13215 54c9eb7 Thanks @jerelmiller! - Ensure the options object for the useQuery, useSuspenseQuery, and useBackgroundQuery hooks provide proper IntelliSense suggestions.
#13229 9a7f65a Thanks @jerelmiller! - Fix refetchOn merging when defaultOptions.watchQuery.refetchOn is set to a non-object value (false, true, or a function) and the per-query refetchOn is an object. Previously the per-query object completely replaced the default so unspecified events fell back to "enabled" regardless of the default.
The defaultOptions value now applies to any event the per-query object does not explicitly configure:
false - unspecified events stay disabledtrue - unspecified events refetchconst client = new ApolloClient({
// ...
defaultOptions: {
watchQuery: {
refetchOn: false,
},
},
});
// Only `windowFocus` refetches. Other events stay disabled per the default.
useQuery(QUERY, { refetchOn: { windowFocus: true } });
#13230 b25b659 Thanks @jerelmiller! - Add the ability to override the default event handler on RefetchEventManager. The default handler runs when no per-source handler is configured for an event. Provide a custom handler via the defaultHandler constructor option or the setDefaultEventHandler instance method.
new RefetchEventManager({
defaultHandler: ({ client, matchesRefetchOn }) => {
return client.refetchQueries({
include: "all",
onQueryUpdated: matchesRefetchOn,
});
},
});
### Minor Changes - #13232 `f1b541f` Thanks @jerelmiller! - Version bump to rc.
f1b541f Thanks @jerelmiller! - Version bump to rc.The defaultOptions value now applies to any event the per-query object does not explicitly configure:
#13229 9a7f65a Thanks @jerelmiller! - Fix refetchOn merging when defaultOptions.watchQuery.refetchOn is set to a non-object value (false, true, or a function) and the per-query refetchOn is an object. Previously the per-query object completely replaced the default so unspecified events fell back to "enabled" regardless of the default.
The defaultOptions value now applies to any event the per-query object does not explicitly configure:
false - unspecified events stay disabledtrue - unspecified events refetchconst client = new ApolloClient({
// ...
defaultOptions: {
watchQuery: {
refetchOn: false,
},
},
});
// Only `windowFocus` refetches. Other events stay disabled per the default.
useQuery(QUERY, { refetchOn: { windowFocus: true } });
#13230 b25b659 Thanks @jerelmiller! - Add the ability to override the default event handler on RefetchEventManager. The default handler runs when no per-source handler is configured for an event. Provide a custom handler via the defaultHandler constructor option or the setDefaultEventHandler instance method.
new RefetchEventManager({
defaultHandler: ({ client, matchesRefetchOn }) => {
return client.refetchQueries({
include: "all",
onQueryUpdated: matchesRefetchOn,
});
},
});
```ts // apollo.d.ts import "@apollo/client";
#13222 b93c172 Thanks @jerelmiller! - Extend the defaultOptions type-safety work to preloadQuery (returned from createQueryPreloader). Defaults declared in DeclareDefaultOptions.WatchQuery now work with preloadQuery to ensure the PreloadedQueryRef'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">
### Patch Changes - #13217 `790f987` Thanks @jerelmiller! - Fix the deprecation for the classic signatures for function overloads that rely on type in
790f987 Thanks @jerelmiller! - Fix the deprecation for the classic signatures for function overloads that rely on type inference from a TypedDocumentNode. The deprecation now only applies to classic signatures that provide explicit type arguments to encourage the use of TypedDocumentNode.### Patch Changes - #13215 `54c9eb7` Thanks @jerelmiller! - Ensure the options object for the useQuery, useSuspenseQuery, and useBackgroundQuery hooks
54c9eb7 Thanks @jerelmiller! - Ensure the options object for the useQuery, useSuspenseQuery, and useBackgroundQuery hooks provide proper IntelliSense suggestions.The RefetchEventManager class handles automatic refetches in response to events. Apollo Client provides built-in sources for window focus and network
#13210 1f9a428 Thanks @jerelmiller! - Add support for automatic event-based refetching, such as window focus.
The RefetchEventManager class handles automatic refetches in response to events. Apollo Client provides built-in sources for window focus and network reconnect as windowFocusSource and onlineSource.
Event refetching is fully opt-in. Create and pass a RefetchEventManager instance to the ApolloClient constructor 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.refetchOn to false and 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.refetchOn and per-query refetchOn options are provided, the objects are merged together.
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 } });
Refetches can be triggered imperatively by calling emit with the event name and its payload (if any).
refetchEventManager.emit("reactNativeAppStatus", "active");
A source that has no automatic detection logic but still wants imperative emit support can be declared as true. Type the event as void to omit the payload argument.
declare module "@apollo/client" {
interface RefetchEvents {
userTriggered: void;
}
}
const refetchEventManager = new RefetchEventManager({
sources: { userTriggered: true },
});
refetchEventManager.emit("userTriggered");
Note: Calling emit on an event without a registered source will log a warning and result in a no-op.
When an event fires, the default handler calls client.refetchQueries({ include: "active" }) filtered by each query's refetchOn setting. You can override the handler for an event to add your own custom filtering. For example, to refetch all queries, including standby queries, 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 RefetchQueriesResult or void. Returning void skips refetching for the event.
The errorPolicy option now flows through to the result types for mutations in the same way it already does for queries:
#13206 08fccab Thanks @jerelmiller! - Extend the defaultOptions type-safety work to client.mutate and useMutation.
The errorPolicy option now flows through to the result types for mutations in the same way it already does for queries:
ApolloClient.MutateResult<TData, TErrorPolicy> maps errorPolicy to the concrete shape of data and error:
"none" → { data: TData; error?: never }"all" → { data: TData | undefined; error?: ErrorLike }"ignore" → { data: TData | undefined; error?: never }client.mutate and useMutation pick up the declared defaultOptions.mutate.errorPolicy and the explicit errorPolicy on each call to narrow return types accordingly.useMutation.Result.error is narrowed to undefined when errorPolicy is "ignore", since client.mutate never resolves with an error in that case.DeclareDefaultOptions.Mutate already accepted errorPolicy; 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 | undefined
Setting errorPolicy on an individual call overrides the default for that call's return type.
Apollo Client 4.2 introduces two signature styles for methods and hooks. All signatures previously present are now "classic" signatures, and a new set
#13132 f3ce805 Thanks @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 using TypedDocumentNode to automatically infer types, which provides more accurate results without any manual annotations.
Modern signatures automatically incorporate your declared defaultOptions into 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 global defaultOptions:
// apollo.d.ts
import "@apollo/client";
declare module "@apollo/client" {
export interface TypeOverrides {
signatureStyle: "modern";
}
}
Users can do a global DeclareDefaultOptions type 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 the defaultOptions you've declared.
#13132 f3ce805 Thanks @phryneas! - Synchronize method and hook return types with defaultOptions.
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 | undefined
While these types are generally correct, if you were to set errorPolicy: 'all' as a default option, the type of result.data for the first query would remain TData instead of changing to TData | undefined to match the runtime behavior.
We are now enforcing that certain defaultOptions types need to be registered globally. This means that if you want to use errorPolicy: '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.data in the above example will correctly be changed to TData | undefined, reflecting the possibility that if an error occurs, data might be undefined. Manually specifying useSuspenseQuery(MY_QUERY, { errorPolicy: "none" }); changes result.data to TData to reflect the local override.
This change means that you will need to declare your default options types in order to use defaultOptions with ApolloClient, 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 defaultOptions value 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 in defaultOptions) 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 ApolloClient constructor accepts any of those values in defaultOptions. The tradeoff is that hook and method return types become more generic. For example, calling useSuspenseQuery without an explicit errorPolicy will 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. So errorPolicy?: "all" | "ignore" has the same effect on return types as errorPolicy: "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", declare errorPolicy: "all" | "ignore" (required) to keep the union narrow and avoid unused values broadening your signatures unnecessarily.
### Patch Changes - #13166 `0537d97` Thanks @jerelmiller! - Release changes in 4.1.5 and 4.1.6.
0537d97 Thanks @jerelmiller! - Release changes in 4.1.5 and 4.1.6.### Minor Changes - #13130 `dd12231` Thanks @jerelmiller! - Improve the accuracy of client.query return type to better detect the current errorPolicy.
dd12231 Thanks @jerelmiller! - Improve the accuracy of client.query return type to better detect the current errorPolicy. The data property is no longer nullable when the errorPolicy is none. This makes it possible to remove the undefined checks or optional chaining in most cases.### Patch Changes - #13203 `099954b` Thanks @copilot-swe-agent! - Remove the workspaces field from the published package.json in dist to avoid Yarn v1
099954b Thanks @copilot-swe-agent! - Remove the workspaces field from the published package.json in dist to avoid Yarn v1 warnings about workspaces requiring private packages.### Patch Changes - #13202 `8a51ea6` Thanks @phryneas! - Ship agent skill for usage with @tanstack/intent — the skill is now bundled in the npm packag
8a51ea6 Thanks @phryneas! - Ship agent skill for usage with @tanstack/intent — the skill is now bundled in the npm package under skills/apollo-client/ and discoverable by intent list.
For more context, see the TanStack Intent QuickStart.### Patch Changes - #13187 `bb3fd9b` Thanks @jerelmiller! - Fix RxJS interop issue with the observable returned by WebSocketLink.
bb3fd9b Thanks @jerelmiller! - Fix RxJS interop issue with the observable returned by WebSocketLink.When both options were combined, the server would return loading: false (because useSSRQuery checks skip first), but the client's getServerSnapshot wa
#13128 6c0b8e4 Thanks @pavelivanov! - Fix useQuery hydration mismatch when ssr: false and skip: true are used together
When both options were combined, the server would return loading: false (because useSSRQuery checks skip first), but the client's getServerSnapshot was returning ssrDisabledResult with loading: true, causing a hydration mismatch.
### Patch Changes - #13155 `3ba1583` Thanks @jerelmiller! - Fix an issue where useQuery would poll with pollInterval when skip was initialized to true
#13155 3ba1583 Thanks @jerelmiller! - Fix an issue where useQuery would poll with pollInterval when skip was initialized to true.
#13135 fd42142 Thanks @jerelmiller! - Fix issue where client.query would apply options from defaultOptions.watchQuery.
### Patch Changes - #13124 `578081f` Thanks @Re-cool! - Ensure PersistedQueryLink merges http and fetchOptions context values instead of overwriting t
Previously, calling dispose() or unsubscribe() on a subscription created by createFetchMultipartSubscription had no effect - the underlying fetch requ
#13111 bf46fe0 Thanks @RogerHYang! - Fix createFetchMultipartSubscription to support cancellation via AbortController
Previously, calling dispose() or unsubscribe() on a subscription created by createFetchMultipartSubscription had no effect - the underlying fetch request would continue running until completion. This was because no AbortController was created or passed to fetch(), and no cleanup function was returned from the Observable.
### Patch Changes - #13105 `8b62263` Thanks @phryneas! - ssrMode, ssrForceFetchDelay or prioritizeCacheValues should not override fetchPolicy: 'cache-
#13105 8b62263 Thanks @phryneas! - ssrMode, ssrForceFetchDelay or prioritizeCacheValues should not override fetchPolicy: 'cache-only', fetchPolicy: 'no-cache', fetchPolicy: 'standby', skip: true, or skipToken when reading the initial value of an ObservableQuery.
#13105 8b62263 Thanks @phryneas! - Fix skipToken in useQuery with prerenderStatic and related SSR functions.
#13105 8b62263 Thanks @phryneas! - Avoid fetches with fetchPolicy: no-cache in useQuery with prerenderStatic and related SSR functions.
### Patch Changes - #13103 `dee7dcf` Thanks @jerelmiller! - Ensure @client fields that are children of aliased server fields are resolved correctly.
dee7dcf Thanks @jerelmiller! - Ensure @client fields that are children of aliased server fields are resolved correctly.```ts function MyComponent() { const [mutate, result] = useMutation(MUTATION, { context: { foo: true }, });
#13043 65e66ca Thanks @jerelmiller! - Support headers transport for enhanced client awareness.
#12927 785e223 Thanks @jerelmiller! - You can now provide a callback function as the context option on the mutate function returned by useMutation. The callback function is called with the value of the context option provided to the useMutation hook. This is useful if you'd like to merge the context object provided to the useMutation hook with a value provided to the mutate function.
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 94ea3e3 Thanks @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 @stream arrays where stream arrays replace the cached arrays.
#12927 96b531f Thanks @jerelmiller! - Don't set the fallback value of a @client field to null when a read function is defined. Instead the read function will be called with an existing value of undefined to allow default arguments to be used to set the returned value.
When a read function is not defined nor is there a defined resolver for the field, warn and set the value to null only in that instance.
#12927 45ebb52 Thanks @jerelmiller! - Add support for from: null in client.watchFragment and cache.watchFragment. When from is null, the emitted result is:
{
data: null,
dataState: "complete",
complete: true,
}
#12926 2b7f2c1 Thanks @jerelmiller! - Support the newer incremental delivery format for the @defer directive implemented in graphql@17.0.0-alpha.9. Import the GraphQL17Alpha9Handler to 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 45ebb52 Thanks @jerelmiller! - Add support for arrays with useFragment, useSuspenseFragment, and client.watchFragment. This allows the ability to use a fragment to watch multiple entities in the cache. Passing an array to from will return data as an array where each array index corresponds to the index in the from array.
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 45ebb52 Thanks @jerelmiller! - Add a getCurrentResult function to the observable returned by client.watchFragment and cache.watchFragment that 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 109efe7 Thanks @jerelmiller! - Add the from option to readFragment, watchFragment, and updateFragment.
#12918 2e224b9 Thanks @jerelmiller! - Add support for the @stream directive on both the Defer20220824Handler and the GraphQL17Alpha2Handler.
[!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 b224efc Thanks @jerelmiller! - InMemoryCache no longer filters out explicitly returned undefined items from read functions for array fields. This now makes it possible to create read functions on array fields that return partial data and trigger a fetch for the full list.
#13058 121a2cb Thanks @jerelmiller! - Add an extensions option to cache.write, cache.writeQuery, and client.writeQuery. This makes extensions available in cache merge functions which can be accessed with the other merge function options.
As a result of this change, any extensions returned in GraphQL operations are now available in merge in the cache writes for these operations.
#12927 96b531f Thanks @jerelmiller! - Add an abstract resolvesClientField function to ApolloCache that can be used by caches to tell LocalState if it can resolve a @client field when a local resolver is not defined.
LocalState will emit a warning and set a fallback value of null when no local resolver is defined and resolvesClientField returns false, or isn't defined. Returning true from resolvesClientField signals that a mechanism in the cache will set the field value. In this case, LocalState won't set the field value.
#13078 bf1e0dc Thanks @phryneas! - Use the default stream merge function for @stream fields only if stream info is present. This change means that using the older Defer20220824Handler will not use the default stream merge function and will instead truncate the streamed array on the first chunk.
#12884 d329790 Thanks @phryneas! - Ensure that PreloadedQueryRef instances are unsubscribed when garbage collected
#13086 1a1d408 Thanks @phryneas! - Change the returned value from null to {} when all fields in a query were skipped.
This also fixes a bug where useSuspenseQuery would suspend indefinitely when all fields were skipped.
#13010 7627000 Thanks @jerelmiller! - Fix an issue where errors parsed from incremental chunks in ErrorLink might throw when using the GraphQL17Alpha9Handler.
#12927 45ebb52 Thanks @jerelmiller! - Deduplicate watches created by useFragment, client.watchFragment, and cache.watchFragment that contain the same fragment, variables, and identifier. This should improve performance in situations where a useFragment or a client.watchFragment is used to watch the same object in multiple places of an application.
#12927 259ae9b Thanks @jerelmiller! - Allow FragmentType not only to be called as FragmentType<TData>, but also as FragmentType<TypedDocumentNode>.
#12925 5851800 Thanks @jerelmiller! - Fix an issue where calling fetchMore with @defer or @stream would not rerender incremental results as they were streamed.
#12927 9e55188 Thanks @jerelmiller! - Truncate @stream arrays only on last chunk by default.
#13083 f3c2be1 Thanks @phryneas! - Expose the ExtensionsWithStreamInfo type for extensions in Cache.writeQuery, Cache.write and Cache.update so other cache implementations also can correctly access them.
#12923 94ea3e3 Thanks @jerelmiller! - Improve the cache data loss warning message when existing or incoming is an array.
#12927 4631175 Thanks @jerelmiller! - Ignore top-level data values on subsequent chunks in incremental responses.
#12927 2be8de2 Thanks @jerelmiller! - Create mechanism to add experimental features to Apollo Client
#12927 96b531f Thanks @jerelmiller! - Ensure LocalState doesn't try to read from the cache when using a no-cache fetch policy.
#12927 bb8ed7b Thanks @jerelmiller! - Ensure an error is thrown when @stream is detected and an incrementalDelivery handler is not configured.
#13053 23ca0ba Thanks @phryneas! - Use memoized observable mapping when using watchFragment, useFragment or useSuspenseFragment.
#12927 44706a2 Thanks @jerelmiller! - Add helper type QueryRef.ForQuery<TypedDocumentNode>
#13082 c257418 Thanks @phryneas! - Pass streamInfo through result extensions as a WeakRef.
#12927 4631175 Thanks @jerelmiller! - Fix the Defer20220824Handler.SubsequentResult type to match the FormattedSubsequentIncrementalExecutionResult type in graphql@17.0.0-alpha.2.
#12927 96b531f Thanks @jerelmiller! - Warn when using a no-cache fetch policy without a local resolver defined. no-cache queries do not read or write to the cache which meant no-cache queries are silently incomplete when the @client field value was handled by a cache read function.
#12927 5776ea0 Thanks @jerelmiller! - Update the accept header used with the GraphQL17Alpha9Handler to multipart/mixed;incrementalSpec=v0.2 to ensure the newest incremental delivery format is requested.
#12927 45ebb52 Thanks @jerelmiller! - DeepPartial<Array<TData>> now returns Array<DeepPartial<TData>> instead of Array<DeepPartial<TData | undefined>>.
#13071 99ffe9a Thanks @phryneas! - prerenderStatic: Expose return value of renderFunction to userland, fix aborted property.
This enables usage of resumeAndPrerender with React 19.2.
#13026 05eee67 Thanks @jerelmiller! - Reduce the number of observables created by watchFragment by 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 7627000 Thanks @jerelmiller! - Handle @stream payloads that send multiple items in the same chunk when using the Defer20220824Handler.
#13010 7627000 Thanks @jerelmiller! - Handle an edge case with the Defer20220824Handler where an error for a @stream item that bubbles to the @stream boundary (such as an item returning null for a non-null array item) would write items from future chunks to the wrong array index. In these cases, the @stream field 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 1e06ad7 Thanks @jerelmiller! - Avoid calling merge functions more than once for the same incremental chunk.
This also fixes a bug where useSuspenseQuery would suspend indefinitely when all fields were skipped.
#13086 1a1d408 Thanks @phryneas! - Change the returned value from null to {} when all fields in a query were skipped.
This also fixes a bug where useSuspenseQuery would suspend indefinitely when all fields were skipped.
#13071 99ffe9a Thanks @phryneas! - prerenderStatic: Expose return value of renderFunction to userland, fix aborted property.
This enables usage of resumeAndPrerender with React 19.2.
### Minor Changes - #13078 `bf1e0dc` Thanks @phryneas! - Use the default stream merge function for @stream fields only if stream info is present. This
bf1e0dc Thanks @phryneas! - Use the default stream merge function for @stream fields only if stream info is present. This change means that using the older Defer20220824Handler will not use the default stream merge function and will instead truncate the streamed array on the first chunk.#13083 f3c2be1 Thanks @phryneas! - Expose the ExtensionsWithStreamInfo type for extensions in Cache.writeQuery, Cache.write and Cache.update so other cache implementations also can correctly access them.
#13082 c257418 Thanks @phryneas! - Pass streamInfo through result extensions as a WeakRef.
#13081 1e06ad7 Thanks @jerelmiller! - Avoid calling merge functions more than once for the same incremental chunk.
As a result of this change, any extensions returned in GraphQL operations are now available in merge in the cache writes for these operations.
#13056 b224efc Thanks @jerelmiller! - InMemoryCache no longer filters out explicitly returned undefined items from read functions for array fields. This now makes it possible to create read functions on array fields that return partial data and trigger a fetch for the full list.
#13058 121a2cb Thanks @jerelmiller! - Add an extensions option to cache.write, cache.writeQuery, and client.writeQuery. This makes extensions available in cache merge functions which can be accessed with the other merge function options.
As a result of this change, any extensions returned in GraphQL operations are now available in merge in the cache writes for these operations.
### Minor Changes - #13043 `65e66ca` Thanks @jerelmiller! - Support headers transport for enhanced client awareness.
65e66ca Thanks @jerelmiller! - Support headers transport for enhanced client awareness.### Minor Changes - #13038 `109efe7` Thanks @jerelmiller! - Add the from option to readFragment, watchFragment, and updateFragment.
109efe7 Thanks @jerelmiller! - Add the from option to readFragment, watchFragment, and updateFragment.### Patch Changes - #13026 `05eee67` Thanks @jerelmiller! - Reduce the number of observables created by watchFragment by reusing existing observables
05eee67 Thanks @jerelmiller! - Reduce the number of observables created by watchFragment by 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.### Patch Changes - #13010 `7627000` Thanks @jerelmiller! - Fix an issue where errors parsed from incremental chunks in ErrorLink might throw when usi
#13010 7627000 Thanks @jerelmiller! - Fix an issue where errors parsed from incremental chunks in ErrorLink might throw when using the GraphQL17Alpha9Handler.
#13010 7627000 Thanks @jerelmiller! - Handle @stream payloads that send multiple items in the same chunk when using the Defer20220824Handler.
#13010 7627000 Thanks @jerelmiller! - Handle an edge case with the Defer20220824Handler where an error for a @stream item that bubbles to the @stream boundary (such as an item returning null for a non-null array item) would write items from future chunks to the wrong array index. In these cases, the @stream field 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.
### Patch Changes - #13009 `259ae9b` Thanks @phryneas! - Allow FragmentType not only to be called as FragmentType , but also as FragmentType . - #1301
`ts { data: null, dataState: "complete", complete: true, } `
#12971 d11eb40 Thanks @jerelmiller! - Add support for from: null in client.watchFragment and cache.watchFragment. When from is null, the emitted result is:
{
data: null,
dataState: "complete",
complete: true,
}
#12971 d11eb40 Thanks @jerelmiller! - Add support for arrays with useFragment, useSuspenseFragment, and client.watchFragment. This allows the ability to use a fragment to watch multiple entities in the cache. Passing an array to from will return data as an array where each array index corresponds to the index in the from array.
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 d11eb40 Thanks @jerelmiller! - Add a getCurrentResult function to the observable returned by client.watchFragment and cache.watchFragment that 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,
// }
#12971 d11eb40 Thanks @jerelmiller! - Deduplicate watches created by useFragment, client.watchFragment, and cache.watchFragment that contain the same fragment, variables, and identifier. This should improve performance in situations where a useFragment or a client.watchFragment is used to watch the same object in multiple places of an application.
#12982 5c56b32 Thanks @jerelmiller! - Ignore top-level data values on subsequent chunks in incremental responses.
#12982 5c56b32 Thanks @jerelmiller! - Fix the Defer20220824Handler.SubsequentResult type to match the FormattedSubsequentIncrementalExecutionResult type in graphql@17.0.0-alpha.2.
#12973 072da24 Thanks @jerelmiller! - Update the accept header used with the GraphQL17Alpha9Handler to multipart/mixed;incrementalSpec=v0.2 to ensure the newest incremental delivery format is requested.
#12971 d11eb40 Thanks @jerelmiller! - DeepPartial<Array<TData>> now returns Array<DeepPartial<TData>> instead of Array<DeepPartial<TData | undefined>>.
```ts function MyComponent() { const [mutate, result] = useMutation(MUTATION, { context: { foo: true }, });
#12959 556e837 Thanks @jerelmiller! - You can now provide a callback function as the context option on the mutate function returned by useMutation. The callback function is called with the value of the context option provided to the useMutation hook. This is useful if you'd like to merge the context object provided to the useMutation hook with a value provided to the mutate function.
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 }),
});
}
// ...
}
1c82eaf Thanks @jerelmiller! - Ensure an error is thrown when @stream is detected and an incrementalDelivery handler is not configured.Your coding agent can read these notes before it upgrades. Set up the MCP server →