github apollographql/apollo-client @apollo/client@4.3.0

2 hours ago

Minor Changes

  • #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:

    • The field policy scalar option and inputObjects type string now use GraphQL list syntax to mark a field as a list of scalars
    • The abstract cache.getScalarForField is now cache.getScalarTypeForField and is expected to return the string representing the scalar type rather than the Scalar instance
    new InMemoryCache({
      scalars: {
        DateTime: new Scalar(/*...*/),
      },
      inputObjects: {
        EventFilter: {
          fields: {
            // Previously only the scalar type was provided
            datesBefore: "DateTime",
    
            // List syntax now required
            datesAfter: "[DateTime]",
            dates2d: "[[DateTime]]",
          },
        },
      },
      typePolicies: {
        Event: {
          fields: {
            // Previously only the scalar type was provided
            datesBefore: {
              scalar: "DateTime",
            },
    
            // List syntax now required
            datesAfter: {
              scalar: "[DateTime]",
            },
            dates2d: {
              scalar: "[[DateTime]]",
            },
          },
        },
      },
    });

    Now it's possible to handle scalars that are represented by arrays:

    const dateTimeRangeScalar = new Scalar<
      [string, string],
      { start: Date; end: Date }
    >({
      parse: ([start, end]) => ({
        start: new Date(start),
        end: new Date(end),
      }),
      serialize: (range) => [range.start.toISOString(), range.end.toISOString()],
      is: (value) => !Array.isArray(value),
    });
    
    const cache = new InMemoryCache({
      scalars: {
        DateTimeRange: dateTimeRangeScalar,
      },
      typePolicies: {
        Event: {
          fields: {
            range: {
              scalar: "DateTimeRange",
            },
          },
        },
      },
    });
    
    const query = gql`
      query {
        event {
          range
        }
      }
    `;
    
    cache.writeQuery({
      query,
      data: {
        event: {
          __typename: "Event",
          // Server returns DateTimeRange as a JSON array
          range: ["2024-01-01T00:00:00Z", "2024-06-01T00:00:00Z"],
        },
      },
    });
    
    const { data } = useQuery(query);
    // => { event: { __typename: "Event", range: { start: Date, end: Date } } }
  • #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
          }
        }
      }
    }
    1. Scenario 1: partial data inside a @defer boundary written to the cache

    Let's say the cache contained the following partial data:

    {
      greeting: {
        __typename: "Greeting",
        recipient: {
          __typename: "Person",
          name: "John Doe",
        },
      },
    };

    After the first chunk arrives from the server, the data looks like the following:

    {
      greeting: {
        __typename: "Greeting",
        message: "Hello, John",
        recipient: {
          __typename: "Person",
          name: "John Doe",
        },
      },
    };

    This data is not 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.

    1. Scenario 2: partial data written to the cache that fulfills the data requirements of the @defer boundary

    Let's say the cache contained the following partial data:

    {
      greeting: {
        __typename: "Greeting",
        recipient: {
          __typename: "Person",
          name: "John Doe",
          email: "john@example.com",
        },
      },
    };

    After the first chunk arrives from the server, the data looks like the following:

    {
      greeting: {
        __typename: "Greeting",
        message: "Hello, John",
        recipient: {
          __typename: "Person",
          name: "John Doe",
          email: "john@example.com",
        },
      },
    };

    In this case, the combination of the first chunk and the partial data in the cache now fulfills the data requirements of the query. Even though the server is still streaming data (NetworkStatus.streaming), we can report this 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 the cache option for the ApolloClient constructor.

  • #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 result
    • cache-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.

Patch Changes

  • #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.

Don't miss a new apollo-client release

NewReleases is sending notifications on new releases.