Thanks to visit codestin.com
Credit goes to www.apollographql.com

Migrating to Apollo Client 4.0


This article walks you through migrating your application to Apollo Client 4.0 from a previous installation of Apollo Client 3.14.

note
If you are migrating from older versions of Apollo Client 3, we generally recommend updating to Apollo Client 3.14 first. Apollo Client 3.14 introduces many deprecations and warnings that will help you prepare your application for a smooth migration to Apollo Client 4.0.

What’s new in 4.0

  1. Framework & Bundle Improvements

  • Framework-agnostic core with React exports moved to @apollo/client/react

  • Better ESM support with exports field in package.json

  • Observable implementation now uses rxjs instead of zen-observable

  • More features are opt-in to reduce bundle size when not used

  1. Enhanced Developer Experience

  • TypeScript: Stricter variable requirements, more precise return types, namespaced types, and customizable core types

  • Error Handling: Unified error property, granular error classes, and consistent errorPolicy behavior for all error types

For a full list of changes, see the changelog.

Installation

caution
Apollo Client 4.0 is a major-version release that includes breaking changes. If you are updating an existing application to use Apollo Client 4.0, see the changelog for details about these changes.

Install Apollo Client 4 along with its peer dependencies with the following command:

sh
npm install @apollo/client graphql rxjs
note
rxjs is a new peer dependency with Apollo Client 4.0.

We recommend following this order to help make your migration as smooth as possible.

Stage 1: Automated updates

Run the codemod first to handle import changes automatically.

You can use the TypeScript-specific codemod or run specific modifications if you prefer a gradual approach.

If you prefer a manual approach, you can still update your imports manually.

Stage 2: Required setup changes

The Apollo Client initialization has changed. Review the new initialization options and update accordingly.

If you use @defer, you need to enable incremental delivery.

If you use data masking, you need to configure the data masking types.

Stage 3: Changes that may require refactoring

These changes may require code refactoring depending on your usage.

Changes affecting most users:

Changes affecting React users:

Changes affecting fewer users:

Codemod

To ease the migration process, we have provided a codemod that automatically updates your codebase.

note
The codemod doesn't update your code with every breaking change. We recommend reading through the full migration guide for any additional changes that need refactoring.

This codemod runs modifications in the following order:

  1. legacyEntrypoints step:
    • Updates CommonJS import statements using the .cjs extension to their associated entry point
      TypeScript
      1import { ApolloClient } from "@apollo/client/main.cjs";
      2import { ApolloClient } from "@apollo/client";
    • Updates ESM import statements using the .js extension to their associated entry point
      TypeScript
      1import { ApolloClient } from "@apollo/client/index.js";
      2import { ApolloClient } from "@apollo/client";
  2. imports step:
    • Updates imports that have moved around
      TypeScript
      1import { useQuery } from "@apollo/client";
      2import { useQuery } from "@apollo/client/react";
    • Updates type references that have moved into namespaces.
      TypeScript
      1import { FetchResult } from "@apollo/client";
      2import { ApolloLink } from "@apollo/client";
      3// ...
      4fn<FetchResult>();
      5fn<ApolloLink.Result>();
  3. links step:
    • Updates the usage of Apollo-provided links to their associated class implementation.
      TypeScript
      1import { createHttpLink } from "@apollo/client";
      2import { HttpLink } from "@apollo/client/link/http";
      3// ...
      4const link = createHttpLink({ uri: "https://example.com/graphql" });
      5const link = new HttpLink({ uri: "https://example.com/graphql" });
      note
      If you use the setContext link, you will need to manually migrate to the SetContextLink class because the order of arguments has changed. This is not automatically performed by the codemod.
    • Updates the usage of from, split and concat functions from @apollo/client/link to use the static methods on the ApolloLink class. For example:
      TypeScript
      1import { from } from "@apollo/client";
      2import { ApolloLink } from "@apollo/client";
      3// ...
      4const link = from([a, b, c]);
      5const link = ApolloLink.from([a, b, c]);
  4. removals step:
    • Updates exports removed from Apollo Client to a special @apollo/client/v4-migration entrypoint. This is a type-only entrypoint that contains doc blocks with migration instructions for each removed item.
      TypeScript
      1import { Concast } from "@apollo/client";
      2import { Concast } from "@apollo/client/v4-migration";
    note
    Any runtime values exported from @apollo/client/v4-migration will throw an error at runtime since their implementations do not exist.
  5. clientSetup step:
    • Moves uri, headers and credentials to the link option and creates a new HttpLink instance
      TypeScript
      1import { HttpLink } from "@apollo/client"; 
      2
      3new ApolloClient({
      4  uri: "/graphql", 
      5  credentials: "include",
      6  headers: {
      7    "x-custom-header"
      8  },
      9  link: new HttpLink({ 
      10    uri: "/graphql",
      11    credentials: "include",
      12    headers: {
      13      "x-custom-header"
      14    },
      15  })
      16})
    • Moves name and version into a clientAwareness option
      TypeScript
      1new ApolloClient({
      2  name: "my-client",
      3  version: "1.0.0",
      4  clientAwareness: {
      5    name: "my-client",
      6    version: "1.0.0",
      7  },
      8});
    • Adds a localState option with a new LocalState instance, moves the resolvers to LocalState, and removes the typeDefs and fragmentMatcher options
      TypeScript
      1import { LocalState } from "@apollo/client/local-state";
      2
      3new ApolloClient({
      4  typeDefs,
      5  fragmentMatcher: () => true,
      6  resolvers: {
      7    /* ... */
      8  },
      9  localState: new LocalState({
      10    resolvers: {
      11      /* ... */
      12    },
      13  }),
      14});
    • Updates the connectToDevTools option to devtools.enabled
      TypeScript
      1new ApolloClient({
      2  connectToDevTools: true,
      3  devtools: {
      4    enabled: true,
      5  },
      6});
    • Renames disableNetworkFetches to prioritizeCacheValues
      TypeScript
      1new ApolloClient({
      2  disableNetworkFetches: true,
      3  prioritizeCacheValues: true,
      4});
    • Adds a template for global type augmentation to re-enable data masking types when the dataMasking option is set to true
      TypeScript
      1new ApolloClient({
      2
      3  /*
      4  Inserted by Apollo Client 3->4 migration codemod.
      5  Keep this comment here if you intend to run the codemod again,
      6  to avoid changes from being reapplied.
      7  Delete this comment once you are done with the migration.
      8  @apollo/client-codemod-migrate-3-to-4 applied
      9  */
      10  dataMasking: true,
      11});
      12
      13
      14/*
      15Start: Inserted by Apollo Client 3->4 migration codemod.
      16Copy the contents of this block into a \`.d.ts\` file in your project
      17to enable data masking types.
      18*/
      19import "@apollo/client";
      20import { GraphQLCodegenDataMasking } from "@apollo/client/masking";
      21declare module "@apollo/client" {
      22  export interface TypeOverrides
      23    extends GraphQLCodegenDataMasking.TypeOverrides {}
      24}
      25/*
      26End: Inserted by Apollo Client 3->4 migration codemod.
      27*/
    • Adds the incrementalHandler option and adds a template for global type augmentation to accordingly type network responses in custom links
      TypeScript
      1import { Defer20220824Handler } from "@apollo/client/incremental";
      2
      3new ApolloClient({
      4
      5  /*
      6  If you are not using the \`@defer\` directive in your application,
      7  Inserted by Apollo Client 3->4 migration codemod.
      8  you can safely remove this option.
      9  */
      10  incrementalHandler: new Defer20220824Handler(),
      11});
      12
      13/*
      14Start: Inserted by Apollo Client 3->4 migration codemod.
      15Copy the contents of this block into a \`.d.ts\` file in your project to enable correct response types in your custom links.
      16If you do not use the \`@defer\` directive in your application, you can safely remove this block.
      17*/
      18import "@apollo/client";
      19import { Defer20220824Handler } from "@apollo/client/incremental";
      20declare module "@apollo/client" {
      21  export interface TypeOverrides extends Defer20220824Handler.TypeOverrides {}
      22}
      23/*
      24End: Inserted by Apollo Client 3->4 migration codemod.
      25*/

Running the codemod

To run the codemod, use the following command:

sh
npx @apollo/client-codemod-migrate-3-to-4 src
note
This command behaves similarly to jscodeshift with a preselected codemod.

For more details on the available options, run the command using the --help option.

sh
npx @apollo/client-codemod-migrate-3-to-4 --help

Using the Codemod with TypeScript

If you have a TypeScript project, we recommend running the codemod against .ts and .tsx files separately as those have slightly overlapping syntax and might otherwise be misinterpreted by the codemod.

sh
npx @apollo/client-codemod-migrate-3-to-4 --parser ts --extensions ts src
npx @apollo/client-codemod-migrate-3-to-4 --parser tsx --extensions tsx src
note
This example targets the src directory with the codemod. Replace src with the file pattern applicable to your file structure if it differs.

Running specific modifications

If you prefer to migrate your application more selectively instead of all at once, you can specify specific modifications using the --codemod option. For example, to run only the imports and links modifications, run the following command:

sh
npx @apollo/client-codemod-migrate-3-to-4 --codemod imports --codemod links src

The following codemods are available:

  • clientSetup - Updates the options provided to the ApolloClient constructor to use their new counterparts

  • legacyEntrypoints - Renames import statements that target .js or .cjs imports to their associated entry point (e.g. @apollo/client/main.cjs becomes @apollo/client)

  • imports - Updates import statements that have moved entry points and updates type references that have moved into a namespace.

  • links - Updates provided Apollo Links to their class-based alternative (e.g. createHttpLink() becomes new HttpLink()).

  • removals - Updates import statements that contain removed features to a special @apollo/client/v4-migration entry point.

Updating imports

Move from manual CJS/ESM imports to exports

note
This section contains instructions for manually updating imports. If you used the Codemod to update your imports, you can safely skip this section.

Apollo Client 4 now includes an exports field in the package's package.json definition. Instead of importing .js or .cjs files directly (e.g. @apollo/client/react/index.js, @apollo/client/react/react.cjs, etc.), you now import from the entrypoint instead (e.g. @apollo/client/react). Your bundler is aware of the different module formats and uses the exports field of the package's package.json to resolve the right format.

List of all changed imports


(click to expand)
The following entry points have been renamed:
Previous entry pointNew entry point
@apollo/client/core@apollo/client
@apollo/client/link/core@apollo/client/link
@apollo/client/react/context@apollo/client/react
@apollo/client/react/hooks@apollo/client/react
@apollo/client/testing/core@apollo/client/testing
The following entry points have been removed:
Previous entry pointReason
@apollo/client/react/componentsThe render prop components were already deprecated in Apollo Client 3.x and have been removed in 4.
@apollo/client/react/hocThe higher order components were already deprecated in Apollo Client 3.x and have been removed in 4.
@apollo/client/react/parserThis module was an implementation detail of the render prop components and HOCs.
@apollo/client/testing/experimentalThis is available as @apollo/graphql-testing-library
@apollo/client/utilities/globalsThis was an implementation detail and has been removed. Some of the exports are now available in other entry points.
@apollo/client/utilities/subscriptions/urqlThis is supported natively by urql and is no longer included.
The following individual imports have been renamed, moved to new entry points and/or moved into namespaces:
Previous entry pointNew entry point
Previous import nameNew import name
@apollo/client(unchanged)
ApolloClientOptionsApolloClient.Options
DefaultOptionsApolloClient.DefaultOptions
DevtoolsOptionsApolloClient.DevtoolsOptions
MutateResultApolloClient.MutateResult
MutationOptionsApolloClient.MutateOptions
QueryOptionsApolloClient.QueryOptions
RefetchQueriesOptionsApolloClient.RefetchQueriesOptions
RefetchQueriesResultApolloClient.RefetchQueriesResult
SubscriptionOptionsApolloClient.SubscribeOptions
WatchQueryOptionsApolloClient.WatchQueryOptions
ApolloQueryResultObservableQuery.Result
FetchMoreOptionsObservableQuery.FetchMoreOptions
SubscribeToMoreOptionsObservableQuery.SubscribeToMoreOptions
@apollo/client@apollo/client/react
ApolloProvider(unchanged)
createQueryPreloader(unchanged)
getApolloContext(unchanged)
skipToken(unchanged)
useApolloClient(unchanged)
useBackgroundQuery(unchanged)
useFragment(unchanged)
useLazyQuery(unchanged)
useLoadableQuery(unchanged)
useMutation(unchanged)
useQuery(unchanged)
useQueryRefHandlers(unchanged)
useReactiveVar(unchanged)
useReadQuery(unchanged)
useSubscription(unchanged)
useSuspenseFragment(unchanged)
useSuspenseQuery(unchanged)
ApolloContextValue(unchanged)
BackgroundQueryHookFetchPolicyuseBackgroundQuery.FetchPolicy
BackgroundQueryHookOptionsuseBackgroundQuery.Options
BaseSubscriptionOptionsuseSubscription.Options
Context(unchanged)
LazyQueryExecFunctionuseLazyQuery.ExecFunction
LazyQueryHookExecOptionsuseLazyQuery.ExecOptions
LazyQueryHookOptionsuseLazyQuery.Options
LazyQueryResultuseLazyQuery.Result
LazyQueryResultTupleuseLazyQuery.ResultTuple
LoadableQueryHookFetchPolicyuseLoadableQuery.FetchPolicy
LoadableQueryHookOptionsuseLoadableQuery.Options
LoadQueryFunctionuseLoadableQuery.LoadQueryFunction
MutationFunction(unchanged)
MutationFunctionOptionsuseMutation.MutationFunctionOptions
MutationHookOptionsuseMutation.Options
MutationResultuseMutation.Result
MutationTupleuseMutation.ResultTuple
NoInfer(unchanged)
OnDataOptionsuseSubscription.OnDataOptions
OnSubscriptionDataOptionsuseSubscription.OnSubscriptionDataOptions
PreloadedQueryRef(unchanged)
PreloadQueryFetchPolicy(unchanged)
PreloadQueryFunction(unchanged)
PreloadQueryOptions(unchanged)
QueryFunctionOptionsuseQuery.Options
QueryHookOptionsuseQuery.Options
QueryRef(unchanged)
QueryReferenceQueryRef
QueryResultuseQuery.Result
QueryTupleuseLazyQuery.ResultTuple
SkipToken(unchanged)
SubscriptionDataOptions(unchanged)
SubscriptionHookOptionsuseSubscription.Options
SubscriptionResultuseSubscription.Result
SuspenseQueryHookFetchPolicyuseSuspenseQuery.FetchPolicy
SuspenseQueryHookOptionsuseSuspenseQuery.Options
UseBackgroundQueryResultuseBackgroundQuery.Result
UseFragmentOptionsuseFragment.Options
UseFragmentResultuseFragment.Result
UseLoadableQueryResultuseLoadableQuery.Result
UseQueryRefHandlersResultuseQueryRefHandlers.Result
UseReadQueryResultuseReadQuery.Result
UseSuspenseFragmentOptionsuseSuspenseFragment.Options
UseSuspenseFragmentResultuseSuspenseFragment.Result
UseSuspenseQueryResultuseSuspenseQuery.Result
VariablesOption(unchanged)
@apollo/client/cache(unchanged)
WatchFragmentOptionsApolloCache.WatchFragmentOptions
WatchFragmentResultApolloCache.WatchFragmentResult
@apollo/client/link@apollo/client/incremental
ExecutionPatchIncrementalResultDefer20220824Handler.SubsequentResult
ExecutionPatchInitialResultDefer20220824Handler.InitialResult
ExecutionPatchResultDefer20220824Handler.Chunk
IncrementalPayloadDefer20220824Handler.IncrementalDeferPayload
PathIncremental.Path
@apollo/client/link(unchanged)
FetchResultApolloLink.Result
GraphQLRequestApolloLink.Request
NextLinkApolloLink.ForwardFunction
OperationApolloLink.Operation
RequestHandlerApolloLink.RequestHandler
@apollo/client/linkgraphql
SingleExecutionResultFormattedExecutionResult
@apollo/client/link/batch(unchanged)
BatchHandlerBatchLink.BatchHandler
@apollo/client/link/context(unchanged)
ContextSetterSetContextLink.LegacyContextSetter
@apollo/client/link/error(unchanged)
ErrorHandlerErrorLink.ErrorHandler
ErrorResponseErrorLink.ErrorHandlerOptions
@apollo/client/link/http@apollo/client/errors
ServerParseError(unchanged)
@apollo/client/link/persisted-queries(unchanged)
ErrorResponsePersistedQueryLink.DisableFunctionOptions
@apollo/client/link/remove-typename(unchanged)
RemoveTypenameFromVariablesOptionsRemoveTypenameFromVariablesLink.Options
@apollo/client/link/utils@apollo/client/errors
ServerError(unchanged)
@apollo/client/link/ws(unchanged)
WebSocketParamsWebSocketLink.Configuration
@apollo/client/react@apollo/client
ContextDefaultContext
@apollo/client/react(unchanged)
QueryReferenceQueryRef
ApolloProviderPropsApolloProvider.Props
BackgroundQueryHookFetchPolicyuseBackgroundQuery.FetchPolicy
BackgroundQueryHookOptionsuseBackgroundQuery.Options
UseBackgroundQueryResultuseBackgroundQuery.Result
LazyQueryExecFunctionuseLazyQuery.ExecFunction
LazyQueryHookExecOptionsuseLazyQuery.ExecOptions
LazyQueryHookOptionsuseLazyQuery.Options
LazyQueryResultuseLazyQuery.Result
LazyQueryResultTupleuseLazyQuery.ResultTuple
QueryTupleuseLazyQuery.ResultTuple
LoadableQueryFetchPolicyuseLoadableQuery.FetchPolicy
LoadableQueryHookFetchPolicyuseLoadableQuery.FetchPolicy
LoadableQueryHookOptionsuseLoadableQuery.Options
LoadQueryFunctionuseLoadableQuery.LoadQueryFunction
UseLoadableQueryResultuseLoadableQuery.Result
MutationFunctionOptionsuseMutation.MutationFunctionOptions
MutationHookOptionsuseMutation.Options
MutationResultuseMutation.Result
MutationTupleuseMutation.ResultTuple
BaseSubscriptionOptionsuseSubscription.Options
OnDataOptionsuseSubscription.OnDataOptions
OnSubscriptionDataOptionsuseSubscription.OnSubscriptionDataOptions
SubscriptionHookOptionsuseSubscription.Options
SubscriptionResultuseSubscription.Result
QueryFunctionOptionsuseQuery.Options
QueryHookOptionsuseQuery.Options
QueryResultuseQuery.Result
SuspenseQueryHookFetchPolicyuseSuspenseQuery.FetchPolicy
SuspenseQueryHookOptionsuseSuspenseQuery.Options
UseSuspenseQueryResultuseSuspenseQuery.Result
UseQueryRefHandlersResultuseQueryRefHandlers.Result
UseFragmentOptionsuseFragment.Options
UseFragmentResultuseFragment.Result
UseReadQueryResultuseReadQuery.Result
UseSuspenseFragmentOptionsuseSuspenseFragment.Options
UseSuspenseFragmentResultuseSuspenseFragment.Result
@apollo/client/react@apollo/client/utilities/internal
NoInfer(unchanged)
VariablesOption(unchanged)
@apollo/client/react/internal@apollo/client/react
PreloadedQueryRef(unchanged)
QueryRef(unchanged)
@apollo/client/testing(unchanged)
MockedRequestMockLink.MockedRequest
MockedResponseMockLink.MockedResponse
MockLinkOptionsMockLink.Options
ResultFunctionMockLink.ResultFunction
@apollo/client/testing@apollo/client/testing/react
MockedProvider(unchanged)
MockedProviderProps(unchanged)
@apollo/client/utilities@apollo/client/utilities/internal
argumentsObjectFromField(unchanged)
AutoCleanedStrongCache(unchanged)
AutoCleanedWeakCache(unchanged)
canUseDOM(unchanged)
checkDocument(unchanged)
cloneDeep(unchanged)
compact(unchanged)
createFragmentMap(unchanged)
createFulfilledPromise(unchanged)
createRejectedPromise(unchanged)
dealias(unchanged)
decoratePromise(unchanged)
DeepMerger(unchanged)
filterMap(unchanged)
getApolloCacheMemoryInternals(unchanged)
getApolloClientMemoryInternals(unchanged)
getDefaultValues(unchanged)
getFragmentDefinition(unchanged)
getFragmentDefinitions(unchanged)
getFragmentFromSelection(unchanged)
getFragmentQueryDocument(unchanged)
getGraphQLErrorsFromResult(unchanged)
getInMemoryCacheMemoryInternals(unchanged)
getOperationDefinition(unchanged)
getOperationName(unchanged)
getQueryDefinition(unchanged)
getStoreKeyName(unchanged)
graphQLResultHasError(unchanged)
hasDirectives(unchanged)
hasForcedResolvers(unchanged)
isArray(unchanged)
isDocumentNode(unchanged)
isField(unchanged)
isNonEmptyArray(unchanged)
isNonNullObject(unchanged)
isPlainObject(unchanged)
makeReference(unchanged)
makeUniqueId(unchanged)
maybeDeepFreeze(unchanged)
mergeDeep(unchanged)
mergeDeepArray(unchanged)
mergeOptions(unchanged)
omitDeep(unchanged)
preventUnhandledRejection(unchanged)
registerGlobalCache(unchanged)
removeDirectivesFromDocument(unchanged)
resultKeyNameFromField(unchanged)
shouldInclude(unchanged)
storeKeyNameFromField(unchanged)
stringifyForDisplay(unchanged)
toQueryResult(unchanged)
DecoratedPromise(unchanged)
DeepOmit(unchanged)
FragmentMap(unchanged)
FragmentMapFunction(unchanged)
FulfilledPromise(unchanged)
IsAny(unchanged)
NoInfer(unchanged)
PendingPromise(unchanged)
Prettify(unchanged)
Primitive(unchanged)
RejectedPromise(unchanged)
RemoveIndexSignature(unchanged)
VariablesOption(unchanged)
@apollo/client/utilities/global@apollo/client/utilities/environment
DEV__DEV__
__DEV__(unchanged)
@apollo/client/utilities/global@apollo/client/utilities/internal/globals
global(unchanged)
maybe(unchanged)
@apollo/client/utilities/global@apollo/client/utilities/invariant
invariant(unchanged)
InvariantError(unchanged)
newInvariantError(unchanged)

Replace removed exports

If you used the Codemod to update your imports, all removed exports moved to the @apollo/client/v4-migration entry point. You should get a TypeScript error everywhere you use a removed export. When you hover over the export, you'll get more information about why the export was removed along with migration instructions.

For a list of all removed imports and recommended actions, see node_modules/@apollo/client/v4-migration.d.ts

Update the initialization of ApolloClient

Several of the constructor options of ApolloClient have changed in Apollo Client 4. This section provides instructions on migrating to the new options when initializing your ApolloClient instance.

This change is performed by the codemod

The uri, headers, and credentials options used to implicitly create an HttpLink have been removed. You now need to create a new HttpLink instance and pass it to the link option of ApolloClient.

TypeScript
1import {
2  ApolloClient,
3  InMemoryCache,
4  HttpLink,
5} from "@apollo/client/core";
6const client = new ApolloClient({
7  // ...
8  link: new HttpLink({
9    uri: "https://example.com/graphql",
10    credentials: "include",
11    headers: { "x-custom-header": "value" },
12  }),
13  // ...
14});

Although the previous options were convenient, it meant all Apollo Client instances were bundled with HttpLink, even if it wasn't used in the link chain. This change enables you to use different terminating links without the increase in bundle size needed to include HttpLink. Most applications that scale moved away from these options anyways as soon as more complex link chains were needed.

Migrate client awareness options

This change is performed by the codemod

The name and version options, which are a part of the client awareness feature, have been moved into the clientAwareness option.

TypeScript
1const client = new ApolloClient({
2  // ...
3  clientAwareness: {
4    name: "My Apollo Client App",
5    version: "1.0.0",
6  },
7  // ...
8});

This change reduced confusion on what the name and version options were meant for and provides better future-proofing for additional features that might be added later.

note
This option only has an effect if you are using the client-awareness-capable HttpLink or BatchHttpLink, or if you manually combine the ClientAwarenessLink with a compatible terminating link.

Update local state

This change is performed by the codemod

When using @client fields, you now need to create a new LocalState instance and provide it as the localState option to the ApolloClient constructor.

TypeScript
1import {
2  ApolloClient,
3  InMemoryCache,
4  LocalState,
5} from "@apollo/client/core";
6const client = new ApolloClient({
7  // ...
8  localState: new LocalState(),
9  // ...
10});

Additionally, if you are using local resolvers with the resolvers option, you need to move the resolvers option to the LocalState constructor instead of the ApolloClient constructor.

TypeScript
1import {
2  ApolloClient,
3  InMemoryCache,
4  LocalState,
5} from "@apollo/client/core";
6const client = new ApolloClient({
7  // ...
8  localState: new LocalState({
9    resolvers: {
10      Query: {
11        myField: () => "Hello World",
12      },
13    },
14  }),
15  // ...
16});

Previously, all Apollo Client instances were bundled with local state management functionality, even if you didn't use the @client directive. This change means local state management is now opt-in and reduces the size of your bundle when you aren't using this feature.

Change connectToDevTools

This change is performed by the codemod

The connectToDevTools option has been replaced with a new devtools option that contains an enabled property.

TypeScript
1const client = new ApolloClient({
2  // ...
3  connectToDevTools: true,
4  devtools: { enabled: true },
5  // ...
6});

Change disableNetworkFetches

This change is performed by the codemod

The disableNetworkFetches option has been renamed to prioritizeCacheValues to better describe its behavior.

TypeScript
1const client = new ApolloClient({
2  // ...
3  disableNetworkFetches: ..., 
4  prioritizeCacheValues: ..., 
5  // ...
6});

Enable incremental delivery (@defer)

This change is performed by the codemod, however you may need to make some additional changes

The incremental delivery protocol implementation is now configurable and requires you to opt-in to use the proper format. If you currently use @defer in your application, you need to configure an incremental handler and provide it to the incrementalHandler option to the ApolloClient constructor.

Since Apollo Client 3 only supported an older version of the specification, initialize an instance of Defer20220824Handler and provide it as the incrementalHandler option.

TypeScript
1import { Defer20220824Handler } from "@apollo/client/incremental";
2const client = new ApolloClient({
3  // ...
4  incrementalHandler: new Defer20220824Handler(),
5  // ...
6});

To provide accurate TypeScript types, you will also need to tell Apollo Client to use the types associated with the Defer20220824Handler in order to provide accurate types for incremental chunks, used with the ApolloLink.Result type. This ensures that you properly handle incremental results in your custom links that might access the result directly.

Create a new .d.ts file in your project (e.g. apollo-client.d.ts) and add the following content:

TypeScript
apollo-client.d.ts
1import { Defer20220824Handler } from "@apollo/client/incremental";
2
3declare module "@apollo/client" {
4  export interface TypeOverrides extends Defer20220824Handler.TypeOverrides {}
5}
note
Apollo Client only provides support for the older incremental specification at this time. Future versions will introduce new incremental handlers to support more recent versions of the specification.

Enable data masking types

This change is performed by the codemod, however you may need to make some additional changes

In Apollo Client 4, the data masking types are now configurable to allow for more flexibility when working with different tools like GraphQL Code Generator or gql.tada - which might have different typings for data masking.

To configure the data masking types to be the same as they were in Apollo Client 3, create a .d.ts file in your project with the following content:

TypeScript
apollo-client.d.ts
1import { GraphQLCodegenDataMasking } from "@apollo/client/masking";
2
3declare module "@apollo/client" {
4  export interface TypeOverrides
5    extends GraphQLCodegenDataMasking.TypeOverrides {}
6}
note
You can combine multiple TypeOverrides of different kinds. For example, you can combine the data masking types with the Defer20220824Handler type overrides from the previous section.
TypeScript
apollo-client.d.ts
1import { Defer20220824Handler } from "@apollo/client/incremental";
2import { GraphQLCodegenDataMasking } from "@apollo/client/masking";
3
4declare module "@apollo/client" {
5  export interface TypeOverrides extends Defer20220824Handler.TypeOverrides {}
6  export interface TypeOverrides
7    extends Defer20220824Handler.TypeOverrides,
8      GraphQLCodegenDataMasking.TypeOverrides {}
9}

Core API changes

New notifyOnNetworkStatusChange default value

The notifyOnNetworkStatusChange option now defaults to true. This affects React hooks that provide this option (e.g. useQuery) and ObservableQuery instances created by client.watchQuery.

This change means you might see loading states more often, especially when used with refetch or other APIs that cause fetches. If this causes issues, you can revert to the v3 behavior by setting the global default back to false.

TypeScript
Reverting notifyOnNetworkStatusChange to Apollo Client 3 behavior
1new ApolloClient({
2  // ...
3  defaultOptions: {
4    watchQuery: {
5      notifyOnNetworkStatusChange: false,
6    },
7  },
8});

Immediate loading status emitted by ObservableQuery

In line with the change to the default notifyOnNetworkStatusChange value, the first value emitted from an ObservableQuery instance (created by client.watchQuery), sets loading to true when the query cannot be fulfilled by data in the cache. Previously, the initial loading state was not emitted.

TypeScript
1const observable = client.watchQuery({ query: QUERY });
2
3observable.subscribe({
4  next: (result) => {
5    // The first result emitted is the loading state
6    console.log(result);
7    /*
8     {
9       data: undefined,
10       dataState: "empty"
11       loading: true,
12       networkStatus: NetworkStatus.loading,
13       partial: false
14     }
15     */
16  },
17});

Tracking of active and inactive queries

In Apollo Client 3, ObservableQuery instances were tracked by the client from the moment they were created until they were unsubscribed from. In cases where the ObservableQuery instance was never subscribed to, this caused memory leaks and unintended behavior with APIs such as refetchQueries when refetching active queries, because the ObservableQuery instances were never properly cleaned up.

In Apollo Client 4, ObservableQuery instances are now tracked by the client only when they are subscribed to until they are unsubscribed from. This allows for more efficient memory management by the garbage collector to free memory if the ObservableQuery instance is never subscribed to and no longer referenced by other code. Additionally, this avoids situations where refetchQueries might execute unnecessary network requests due to the tracking behavior.

As a result of this change, the definitions for active and inactive queries have changed.

  • A query is considered active if it is observed by at least one subscriber and not in standby

  • A query is considered inactive if it is observed by at least one subscriber and in standby

  • All other ObservableQuery instances that don't have at least one subscriber are not tracked or accessible through the client

note
A query is in standby if it is skipped with the skip option or skipToken in a React hook, or if the fetchPolicy is set to standby.

This change affects the queries returned by client.getObservableQueries or the queries fetched by refetchQueries as these no longer include ObservableQuery instances without subscribers.

Removal of the canonizeResults option

The canonizeResults option has been removed because it caused memory leaks. If you use canonizeResults, you may see some changes to the object identity of some objects that were previously referentially equal.

TypeScript
1useQuery(QUERY, {
2  canonizeResults: true,
3});

New error handling

Error handling in Apollo Client 4 has changed significantly to be more predictable and intuitive.

tip
We recommend reading the Error handling guide which lists all of the available error classes and provides details on how to check for specific kinds of errors. The following sections only detail the necessary requirements to migrate your existing code but does not cover general error handling.

Unification of the error property

In Apollo Client 3, many result types returned both the error and errors properties. This includes the result emitted from ObservableQuery and some React hooks such as useQuery. This made it difficult to determine which property should be used to determine the source of the error. This was further complicated by the fact that the errors property was set only when errorPolicy was all!

Apollo Client 4 unifies all errors into a single error property. If you check for errors, remove this check and use error instead.

TypeScript
1const { error, errors } = useQuery(QUERY);
2const { error } = useQuery(QUERY);
3
4if (error) {
5  // ...
6} else if (errors) {
7  // ...
8}

Removal of ApolloError

In Apollo Client 3, every error was wrapped in an ApolloError instance. This made it difficult to debug the source of the error, especially when reported by error tracking services, because the stack trace pointed to the ApolloError class. Additionally, it was unclear whether error types wrapped in ApolloError were mutually exclusive because it contained graphQLErrors, protocolErrors, networkError, and clientErrors properties that were all optional.

Apollo Client 4 removes the ApolloError class entirely.

Migrate from graphQLErrors

GraphQL errors are now encapsulated in a CombinedGraphQLErrors instance. You can access the raw GraphQL errors with the errors property. To migrate, use CombinedGraphQLErrors.is(error) to first check if the error is caused by one or more GraphQL errors, then access the errors property.

TypeScript
1import { CombinedGraphQLErrors } from "@apollo/client"; 
2
3const { error } = useQuery(QUERY);
4
5if (error.graphQLErrors) { 
6  error.graphQLErrors.map((graphQLError) => {/* ... */}); 
7if (CombinedGraphQLErrors.is(error)) { 
8  error.errors.map((graphQLError) => {/* ... */}); 
9}

Migrate from networkError

Network errors are no longer wrapped and instead returned as-is. This makes it easier to debug the source of the error as the stack trace now points to the location of the error.

To migrate, use the error property directly.

TypeScript
1const { error } = useQuery(QUERY);
2
3if (error.networkError) { 
4  console.log(error.networkError.message); 
5if (error) { 
6  console.log(error.message); 
7}

Migrate from protocolErrors

Protocol errors are now encapsulated in a CombinedProtocolErrors instance. You can access the raw protocol errors with the errors property. To migrate, use CombinedProtocolErrors.is(error) to first check if the error is caused by one or more protocol errors, then access the errors property.

TypeScript
1import { CombinedProtocolErrors } from "@apollo/client"; 
2
3const { error } = useQuery(QUERY);
4
5if (error.protocolErrors) { 
6  error.graphQLErrors.map((graphQLError) => {/* ... */}); 
7if (CombinedProtocolErrors.is(error)) { 
8  error.errors.map((graphQLError) => {/* ... */}); 
9}

Migrate from clientErrors

The clientErrors property was not used by Apollo Client and therefore has no replacement. Any non-GraphQL errors or non-protocol errors are passed through as-is.

Errors as guaranteed error-like objects

Apollo Client 4 guarantees that the error property is an ErrorLike object, an object with a message and name property. This avoids the need to check the type of error before consuming it. To make such a guarantee, thrown non-error-like values are wrapped.

String errors

Strings thrown as errors are wrapped in an Error instance for you. The string is set as the error's message.

TypeScript
1const client = new ApolloClient({
2  link: new ApolloLink(() => {
3    return new Observable((observer) => {
4      // Oops we sent a string instead of wrapping it in an `Error`
5      observer.error("Test error");
6    });
7  }),
8});
9
10// ...
11
12const { error } = useQuery(query);
13
14// `error` is `new Error("Test error")`
15console.log(error.message);

Non-error-like types

All other object types (e.g. symbols, arrays, etc.) or primitives thrown as errors are wrapped in an UnconventionalError instance with the cause property set to the original object. Additionally, UnconventionalError sets its name to UnconventionalError and its message to "An error of unexpected shape occurred.".

TypeScript
1const client = new ApolloClient({
2  link: new ApolloLink(() => {
3    return new Observable((observer) => {
4      // Oops we sent a string instead of wrapping it in an `Error`
5      observer.error({ message: "Not a proper error type" });
6    });
7  }),
8});
9
10// ...
11
12const { error } = useQuery(query);
13
14// `error` is an `UnconventionalError` instance
15console.log(error.message); // => "An error of unexpected shape occurred."
16// The `cause` returns the original object that was thrown
17console.log(error.cause); // => { message: "Not a proper error type" }

Network errors adhere to the errorPolicy

Apollo Client 3 always treated network errors as if the errorPolicy was set to none. This meant network errors were always thrown even if a different errorPolicy was specified.

Apollo Client 4 unifies the behavior of network errors and GraphQL errors so that network errors now adhere to the errorPolicy. Network errors now resolve promises when the errorPolicy is set to anything other than none.

errorPolicy: all

The promise is resolved. The error is accessible on the error property.

TypeScript
1const { data, error } = await client.query({ query, errorPolicy: "all" });
2
3console.log(data); // => undefined
4console.log(error); // => new Error("...")
note
Network errors always set data to undefined.

errorPolicy: ignore

The promise is resolved and the error is ignored.

TypeScript
1const { data, error } = await client.query({ query, errorPolicy: "ignore" });
2
3console.log(data); // => undefined
4console.log(error); // => undefined
note
data is always set to undefined because a GraphQL result wasn't returned.

Network errors never terminate ObservableQuery

In Apollo Client 3, network errors emitted an error notification on the ObservableQuery instance. Because error notifications terminate observables, this meant you needed special handling to restart the ObservableQuery instance so that it listened for new results. Network errors were emitted to the error callback.

Apollo Client 4 changes this and no longer emits an error notification. This ensures ObservableQuery doesn't terminate on errors and can continue listening for new results, such as calling refetch after an error.

Access the error on the error property emitted in the next callback. It is safe to remove the error callback entirely as it is no longer used.

TypeScript
1const observable = client.watchQuery({ query });
2
3observable.subscribe({
4  next: (result) => {
5    if (result.error) {
6      // handle error
7    }
8    // ...
9  },
10  error: (error) => {
11    // handle error
12  },
13});

ServerError and ServerParseError are error classes

Apollo Client 3 threw special ServerError and ServerParseError types when the HTTP response was invalid. These types were plain Error instances with some added properties.

Apollo Client 4 turns these types into proper error classes. You can check for these types using the .is static method available on each error class.

TypeScript
1const { error } = useQuery(QUERY);
2
3if (ServerError.is(error)) {
4  // handle the server error
5}
6
7if (ServerParseError.is(error)) {
8  // handle the server parse error
9}

ServerError no longer parses the response text as JSON

When a ServerError was thrown, such as when the server returned a non-2xx status code, Apollo Client 3 tried to parse the raw response text into JSON to determine if a valid GraphQL response was returned. If successful, the parsed JSON value was set as the result property on the error.

Apollo Client 4 more strictly adheres to the GraphQL over HTTP specification and does away with the automatic JSON parsing. As such, the result property is no longer accessible and is replaced by the bodyText property which contains the value of the raw response text.

If you relied on the automatic JSON parsing, you will need to JSON.parse the bodyText instead.

TypeScript
1const { error } = useQuery(QUERY);
2
3if (ServerError.is(error)) {
4  try {
5    const json = JSON.parse(error.bodyText);
6  } catch (e) {
7    // handle invalid JSON
8  }
9}

Updates to React APIs

A note about hook usage

Apollo Client 4 is more opinionated about how to use it. We believe that React hooks should be used to synchronize data with your component and avoid side effects that don't achieve this goal. As a result, we now recommend the use of core APIs directly in several use cases where synchronization with a component is not needed or intended.

As an example, if you used useLazyQuery to execute queries but don't read the state values returned in the result tuple, we recommend that you now use client.query directly. This avoids unnecessary re-renders in your component for state that you don't consume.

TypeScript
1const [execute] = useLazyQuery(MY_QUERY);
2// ...
3await execute({ variables });
4const client = useApolloClient();
5// ...
6await client.query({ query: MY_QUERY, variables });

Changes to useLazyQuery

useLazyQuery has been rewritten to have more predictable and intuitive behavior based on years of user feedback. In Apollo Client 3, the useLazyQuery behaved like the useQuery hook after the execute function was called the first time. Changes to options often triggered unintended network requests as a result of this behavior.

In Apollo Client 4, useLazyQuery focuses on user interaction and more predictable data synchronization with the component. Network requests are now initiated only when the execute function is called. This makes it safe to re-render your component with new options provided to useLazyQuery without the unintended network requests. New options are used the next time the execute function is called.

Changes to the variables and context options

The useLazyQuery hook no longer accepts the variables or context options. These options have been moved to the execute function instead. This removes the variable merging behavior which was confusing and hard to predict with the new behavior.

To migrate, move the variables and context options from the hook to the execute function:

TypeScript
1const [execute, { data }] = useLazyQuery(QUERY, {
2  variables: {/* ... */},
3  context: { /* ... */},
4});
5
6function onUserInteraction(){
7  execute({
8    variables: {/* ... */}, 
9    context: {/* ... */}, 
10  })
11}

Changes to the execute function

The execute function no longer accepts any other options than variables and context. Those options should be passed directly to the hook instead.

TypeScript
1const [execute, { data }] = useLazyQuery(QUERY, {
2  fetchPolicy: "no-cache", 
3});
4// ...
5function onUserInteraction(){
6  execute({
7    fetchPolicy: "no-cache", 
8  })
9}

If you need to change the value of an option, rerender the component with the new option value before calling the execute function again.

Executing the query during render

Calling the execute function of useLazyQuery during render is no longer allowed and will now throw an error.

TypeScript
1function MyComponent() {
2  const [execute, { data }] = useLazyQuery(QUERY);
3
4  // This throws an error
5  execute();
6
7  return /* ... */;
8}

We recommend instead migrating to useQuery which executes the query during render automatically.

TypeScript
1function MyComponent() {
2  const [execute, { data }] = useLazyQuery(QUERY);
3  execute();
4
5  const { data } = useQuery(QUERY);
6
7  return /* ... */;
8}
tip
We don't recommend calling the execute function in a useEffect hook as a replacement. Instead, migrate to useQuery. If you need to hold the execution of the query beyond the initial render, consider using the skip option.
TypeScript
1function MyComponent({ dependentValue }) {
2  const [execute, { data }] = useLazyQuery(QUERY);
3
4  useEffect(() => {
5    if (dependentValue) {
6      execute();
7    }
8  }, [dependentValue]);
9
10  const { data } = useQuery(QUERY, { skip: !dependentValue });
11
12  return /* ... */;
13}
Calling execute inside a useEffect delays the execution of the query unnecessarily. Reserve the use of useLazyQuery for user interaction in callback functions.
A note about SSR

This change also means that it is no longer possible to make queries with useLazyQuery during SSR. If you need to execute the query during render, use useQuery instead.

TypeScript
1function MySSRComponent() {
2  const [execute, { data, loading, error }] = useLazyQuery(QUERY);
3  execute();
4
5  const { data, loading, error } = useQuery(QUERY);
6
7  // ...
8}

Removal of onCompleted and onError callbacks

The onCompleted and onError callbacks have been removed from the hook options. If you need to execute a side effect when the query completes, await the promise returned by the execute function and perform side effects there.

TypeScript
1const [execute] = useLazyQuery(QUERY, {
2  onCompleted(data) { /* handle success */ }, 
3  onError(error) { /* handle error */ }, 
4});
5
6async function onUserInteraction(){
7  await execute({ variables })
8
9  try {
10    const { data } = await execute({ variables })
11    // handle success
12  } catch (error) {
13    // handle error
14  }
15}

Changes to behavior for in-flight queries

In-flight queries are now aborted more frequently. This occurs under two conditions:

  • The component unmounts while the query is in flight

  • A new query is started by calling the execute function while a previous query is in flight

In each of these cases, the promise returned by the execute function is rejected with an AbortError. This change means it is no longer possible to run multiple queries simultaneously.

In some cases, you may need the query to run to completion, even if a new query is started or your component unmounts. In these cases, you can call the .retain() function on the promise returned by the execute function.

TypeScript
1const [execute] = useLazyQuery(QUERY);
2
3async function onUserInteraction() {
4  try {
5    const { data } = await execute({ variables });
6    const { data } = await execute({ variables }).retain();
7  } catch (error) {}
8}
note
Even though in-flight queries run to completion when using .retain(), the result returned from the hook only reflects state from the latest call of the execute function. Any previously retained queries are not synchronized to the hook state.
tip
Usage of .retain() can be a sign that you are using useLazyQuery to trigger queries and are not interested in synchronizing the result with your component. In that case, we recommend using client.query directly.
TypeScript
1const [execute] = useLazyQuery(QUERY);
2const client = useApolloClient();
3
4async function onUserInteraction() {
5  try {
6    const { data } = await execute({ variables }).retain();
7    const { data } = await client.query({ query: QUERY, variables });
8  } catch (error) {}
9}

Changes to useMutation

useMutation has been modified to work with our philosophy that React hooks should be used to synchronize hook state with your component.

Removal of the ignoreResults option

The ignoreResults option of useMutation has been removed. If you want to trigger a mutation, but are not interested in synchronizing the result with your component, use client.mutate directly.

TypeScript
1const [mutate] = useMutation(MY_MUTATION, { ignoreResults: true });
2const client = useApolloClient();
3// ...
4await mutate({ variables });
5await client.mutate({ mutation: MY_MUTATION, variables });

Changes to useQuery

New notifyOnNetworkStatusChange default value

The notifyOnNetworkStatusChange option now defaults to true. This change means you might see loading states more often, especially when used with refetch or other APIs that cause fetches. If this causes issues, you can revert to the v3 behavior by setting the global default back to false.

TypeScript
1new ApolloClient({
2  // ...
3  defaultOptions: {
4    watchQuery: {
5      notifyOnNetworkStatusChange: false,
6    },
7  },
8});

Removal of the onCompleted and onError callbacks

The onCompleted and onError callback options have been removed. Their behavior was ambiguous and made it easy to introduce subtle bugs in your application.

You can read more about this decision and recommendations on what to do instead in the related GitHub issue.

Changes to preloadQuery

toPromise moved from queryRef to the preloadQuery function

The toPromise method now exists as a property on the preloadQuery function instead of on the queryRef object. This change makes queryRef objects more serializable for SSR environments.

To migrate, call preloadQuery.toPromise and pass it the queryRef.

TypeScript
1function loader() {
2  const queryRef = preloadQuery(QUERY, options);
3
4  return queryRef.toPromise();
5  return preloadQuery.toPromise(queryRef);
6}

Apollo Client 4 comes with a lot of TypeScript improvements to the hook types. If you use generated hooks from a tool such as GraphQL Codegen's @graphql-codegen/typescript-react-apollo plugin, you will not benefit from these improvements since the generated hooks are missing the necessary function overloads to provide critical type safety. We recommend that you stop using generated hooks immediately and create a migration strategy to move away from them.

Use the hooks exported from Apollo Client directly with TypedDocumentNode instead.

note
The GraphQL Codegen team has created a codemod that helps you migrate away from generated hooks. Learn more about migrating your code using the new codemod by watching the tutorial video.

Note that the codemod is a separate package and not maintained by the Apollo Client team. Generally, we recommend that you use a manual codegen configuration over the client preset as shown in the video because the client preset includes some features that are incompatible with Apollo Client. If you choose to use the client preset, see our recommended configuration for the client preset. In the long term, consider using the plugins directly with our recommended starter configuration.

TypeScript changes

Namespaced types

Most types are now colocated with the API that they belong to. This makes them more discoverable, adds consistency to the naming of each type, and provides clear ownership boundaries.

Some examples:

  • FetchResult is now ApolloLink.Result

  • ApolloClientOptions is now ApolloClient.Options

  • QueryOptions is now ApolloClient.QueryOptions

  • ApolloQueryResult is now ObservableQuery.Result

  • QueryHookOptions is now useQuery.Options

  • QueryResult is now useQuery.Result

  • LazyQueryResult is now useLazyQuery.Result

Many of the old, most-used types are still available in Apollo Client 4 (including the examples above), but are now deprecated in favor of the new namespaced types. We suggest running the codemod to update your code to use the new namespaced types.

Removal of <TContext> generic argument

Many APIs in Apollo Client 3 provided a TContext generic argument that allowed you to provide the type for the context option. However, this generic argument was not consistently applied across all APIs that provided a context option and was easily forgotten.

Apollo Client 4 removes the TContext generic argument in favor of using declaration merging with the DefaultContext interface to provide types for the context option. This provides type safety throughout all Apollo Client APIs when using context.

To define types for your custom context properties, create a TypeScript file and define the DefaultContext interface.

TypeScript
apollo-client.d.ts
1// This import is necessary to ensure all Apollo Client imports
2// are still available to the rest of the application.
3import "@apollo/client";
4
5declare module "@apollo/client" {
6  interface DefaultContext {
7    myProperty?: string;
8    requestId?: number;
9  }
10}

Removal of <TCacheShape> generic argument

The TCacheShape generic argument has been removed from the ApolloClient constructor. Most users set this type to any which provided little benefit for type safety. APIs that previously relied on TCacheShape are now set to unknown.

To migrate, remove the TCacheShape generic argument when initializing ApolloClient.

TypeScript
1new ApolloClient<any>({ 
2new ApolloClient({ 
3  // ...
4});

All links are now available as classes. The old link creator functions are still provided, but are now deprecated and will be removed in a future major version.

Old link creatorNew link class
setContextSetContextLink
onErrorErrorLink
createHttpLinkHttpLink
createPersistedQueryLinkPersistedQueryLink
removeTypenameFromVariablesRemoveTypenameFromVariablesLink
caution
Pay attention when migrating from the old setContext creator to the new SetContextLink class. The argument order for the callback function has changed. To migrate, swap the order of the prevContext and operation arguments.
TypeScript
1import { setContext } from "@apollo/client/link/context"; 
2import { SetContextLink } from "@apollo/client/link/context"; 
3
4const authLink = setContext((operation, prevContext) => { /*...*/ } 
5const authLink = new SetContextLink((prevContext, operation) => { /*...*/ }  

Deprecation of bare empty, from, concat, and split functions

This change is performed by the codemod

The bare empty, from, concat, and split functions exported from @apollo/client and @apollo/client/link are now deprecated in favor of using the equivalent static methods on the ApolloLink class.

TypeScript
1import { empty, from, concat, split } from "@apollo/client";
2import { ApolloLink } from "@apollo/client";
3
4const link = empty();
5const link = ApolloLink.empty();
6
7from([linkA, linkB]);
8ApolloLink.from([linkA, linkB]);
9
10concat(linkA, linkB);
11ApolloLink.concat(linkA, linkB);
12
13split((operation) => check(operation), linkA, linkB);
14ApolloLink.split((operation) => check(operation), linkA, linkB);

Updated concat behavior

The static ApolloLink.concat method and the ApolloLink.prototype.concat method now accept a dynamic number of arguments to allow for concatenation with more than one link. This aligns the API with more familiar APIs such as Array.prototype.concat which accepts a variable number of arguments and provides more succinct code.

TypeScript
1link1.concat(link2).concat(link3);
2link1.concat(link2, link3);

Deprecation of ApolloLink.concat

The static ApolloLink.concat is now deprecated in favor of ApolloLink.from. With the change to allow for a variable number of arguments, ApolloLink.concat(a, b) is now an alias for ApolloLink.from([a, b]) and provides no additional benefit. ApolloLink.concat will be removed in a future major version.

TypeScript
1import { ApolloLink } from "@apollo/client/link";
2
3ApolloLink.concat(firstLink, secondLink);
4ApolloLink.from([firstLink, secondLink]);

These methods previously accepted ApolloLink instances or plain request handler functions as arguments. These APIs have been updated to require ApolloLink instances.

To migrate, wrap the request handler functions in an ApolloLink instance.

TypeScript
1ApolloLink.from(
2  (operation, forward) => {
3    /*...*/
4  },
5  new ApolloLink((operation, forward) => {
6    /*...*/
7  }),
8  nextLink
9);

The ErrorLink (previously created with onError) now uses a single error property instead of separate graphQLErrors, networkError, and protocolErrors.

With built-in error classes, use ErrorClass.is to check for specific error types. For external error types like TypeError, use instanceof or the more modern Error.isError in combination with a check for error.name to check the kind of error.

TypeScript
1import {
2  onError,
3  ErrorLink,
4} from "@apollo/client/link/error";
5import { CombinedGraphQLErrors, ServerError } from "@apollo/client";
6
7const errorLink = onError(
8  ({ graphQLErrors, networkError, protocolErrors, response }) => {
9    if (graphQLErrors) {
10      graphQLErrors.forEach(({ message }) =>
11        console.log(`GraphQL error: ${message}`)
12      );
13    }
14    if (networkError) {
15      console.log(`Network error: ${networkError.message}`);
16    }
17  }
18);
19const errorLink = new ErrorLink(({ error, result }) => {
20  if (CombinedGraphQLErrors.is(error)) {
21    error.errors.forEach(({ message }) =>
22      console.log(`GraphQL error: ${message}`)
23    );
24  } else if (ServerError.is(error)) {
25    console.log(`Server error: ${error.message}`);
26  } else if (error) {
27    console.log(`Other error: ${error.message}`);
28  }
29});
note
If the error thrown in the link chain is not an error-like object, it will be wrapped in an UnconventionalError class - so it is always safe to access error.message and error.name on the error object.

Changes to operation

operationName as undefined for anonymous queries

The operationName is now set to undefined when executing an anonymous query. Anonymous queries were previously set as an empty string.

If you use string methods on the operationName, you may need to check for undefined first.

New operation.operationType property

In some cases, you might have needed to determine the GraphQL operation type of the request. This is common when using ApolloLink.split to route subscription operations to a WebSocket-based link.

A new operationType property has been introduced to reduce the boilerplate needed to determine the operation type of the GraphQL document.

TypeScript
1import { getMainDefinition } from "@apollo/client";
2import { OperationTypeNode } from "graphql";
3
4new ApolloLink((operation, forward) => {
5  const definition = getMainDefinition(query);
6  const isSubscription =
7    definition.kind === "OperationDefinition" &&
8    definition.operation === "subscription";
9
10  const isSubscription =
11    operation.operationType === OperationTypeNode.SUBSCRIPTION;
12});

Type safety

The context type can be extended by using declaration merging to allow you to define custom properties used by your links.

For example, to make your context type-safe for usage with HttpLink, place this snippet in a .d.ts file in your project:

TypeScript
apollo-client.d.ts
1import { HttpLink } from "@apollo/client";
2
3declare module "@apollo/client" {
4  interface DefaultContext extends HttpLink.ContextOptions {}
5}

For more information on how to setup context types, see the TypeScript guide.

Changes to operation.getContext

Readonly context

The context object returned by operation.getContext() is now frozen and Readonly. This prevents mutations to the context object that aren't propagated to downstream links and could cause subtle bugs.

To make changes to context, use operation.setContext().

Removal of the cache property

Apollo Client 3 provided a reference to the cache on the context object for use with links via the cache property. This property has been removed and replaced with a client property on the operation which references the client instance that initiated the request.

Access the cache through the client property instead.

TypeScript
1new ApolloLink((operation, forward) => {
2  const cache = operation.getContext().cache;
3  const cache = operation.client.cache;
4});

Removal of the getCacheKey function

Apollo Client 3 provided a getCacheKey function on the context object to obtain cache IDs. This function has been removed.

Use the client property to get access to the cache.identify function to identify an object.

TypeScript
1new ApolloLink((operation, forward) => {
2  const cacheID = operation.getContext().getCacheKey(obj);
3  const cacheID = operation.client.cache.identify(obj);
4});

execute now requires the client

If you use the execute function directly to run the link chain, the execute function now requires a 3rd argument that provides the ApolloClient instance that represents the client that initiated the request.

Provide a context argument with the client option to the 3rd argument of the execute function.

TypeScript
1import { execute } from "@apollo/client";
2
3const client = new ApolloClient({
4  // ...
5});
6
7execute(
8  link,
9  { query /* ... */ },
10  { client }
11);
tip
If you use the execute function to unit-test custom links, provide a dummy ApolloClient instance. The ApolloClient instance does not need any special configuration unless your unit tests require it.

Observable API changes

In Apollo Client 4, the underlying Observable implementation has changed from using zen-observable to using rxjs Observable instances.

This can affect custom link implementations that rely on the Observable API.

If you were previously using the map, filter, reduce, flatMap or concat methods on the Observable instances, you will need to update your code to use the rxjs equivalents.

Additionally, if you were using Observable.of or Observable.from, you will need to use the rxjs of or from functions instead.

TypeScript
1import { of, from, map } from "rxjs";
2
3Observable.of(1, 2, 3);
4of(1, 2, 3);
5Observable.from([1, 2, 3]);
6from([1, 2, 3]);
7observable.map((x) => x * 2);
8observable.pipe(map((x) => x * 2));

A good way to find the operator you need is to follow the rxjs operator decision tree.

GraphQL over HTTP spec compatibility

The HttpLink and BatchHttpLink links have been updated to add stricter compatibility with the GraphQL over HTTP specification.

This change means:

  • The default Accept header is now application/graphql-response+json,application/json;q=0.9 for all outgoing requests

  • The application/graphql-response+json media type is now supported and the client handles the response according to the application/graphql-response+json behavior

  • The application/json media type behaves according to the application/json behavior

The client now does the following when application/json media type is returned.

  • The client will throw a ServerError when the server encodes a content-type using application/json and returns a non-200 status code

  • The client will now throw a ServerError when the server encodes using any other content-type and returns a non-200 status code

note
If you use a testing utility to mock requests in your test, you may experience different behavior than production if your testing utility responds as application/json but your production server responds as application/graphql-response+json. If a content-type header is not set, the client interprets the response as application/json by default.

Local state changes

Local state management (using @client fields) has been removed from core and is now an opt-in feature. This change helps reduce the bundle size of your application if you don't use local state management features.

To opt-in to use local state management with the @client directive, initialize an instance of LocalState and provide it as the localState option to the ApolloClient constructor. Apollo Client will throw an error if a @client field is used and localState is not provided.

TypeScript
1import { LocalState } from "@apollo/client/local-state";
2
3new ApolloClient({
4  // ...
5  localState: new LocalState(),
6  // ...
7});

Local resolvers

The resolvers option has been removed from the ApolloClient constructor and is now part of the LocalState class. Move local resolvers to the LocalState class.

TypeScript
1new ApolloClient({
2  resolvers: {...}, 
3  localState: new LocalState({
4    resolvers: {...} 
5  })
6});

Now that local resolvers are part of the LocalState class, the following resolver methods were removed from the ApolloClient class:

  • addResolvers

  • getResolvers

  • setResolvers

If you add resolvers after the initialization of the client, you may continue to do so with your LocalState instance using the addResolvers method.

TypeScript
1const client = new ApolloClient({
2  // ...
3});
4
5client.addResolvers(resolvers);
6client.localState.addResolvers(resolvers);

getResolvers and setResolvers have been removed without a replacement. Stop using them.

TypeScript
1const client = new ApolloClient({
2  // ...
3});
4
5const resolvers = client.getResolvers();
6client.setResolvers(resolvers);

Resolver context

The context argument (i.e. the 3rd argument) passed to resolver functions has been updated. Apollo Client 3 spread request context and replaced any passed-in client and cache properties.

Apollo Client 4 moves the request context to the requestContext key to avoid name clashes and updates the shape of the object. The context argument is now provided with the following object:

TypeScript
1{
2  // the request context. By default `TContextValue` is of type `DefaultContext`,
3  // but can be changed if a `context` function is provided.
4  requestContext: TContextValue,
5  // The client instance making the request
6  client: ApolloClient,
7  // Whether the resolver is run as a result of gathering exported variables
8  // or resolving the value as part of the result
9  phase: "exports" | "resolve"
10}

If you accessed properties from the request context, you will now need to read those properties from the requestContext key. If you used the cache property, you will need to get the cache from the client property.

TypeScript
1new LocalState({
2  resolvers: {
3    MyType: {
4      myLocalField: (parent, args, { someValue, cache }) { 
5      myLocalField: (parent, args, { requestContext, client }) { 
6        const someValue = requestContext.someValue; 
7        const cache = client.cache; 
8      }
9    }
10  }
11})

Thrown errors

Errors thrown in local resolvers are now handled properly and predictably. Errors are now added to the errors array in the GraphQL response and the field value is set to null.

The following example throws an error in a local resolver.

TypeScript
1new LocalState({
2  resolvers: {
3    Query: {
4      localField: () => {
5        throw new Error("Could not get localField");
6      },
7    },
8  },
9});

This results in the following result:

TypeScript
1{
2  data: {
3    localField: null,
4  },
5  errors: [
6    {
7      message: "Could not get localField",
8      path: ["localField"],
9      extensions: {
10        localState: {
11          resolver: "Query.localField",
12          cause: new Error("Could not get localField"),
13        },
14      },
15    },
16  ],
17};

As a result of this change, it is now safe to throw, or allow thrown errors in your resolvers. If your local resolvers catch errors to avoid issues in Apollo Client 3, these can be removed in cases where you want the error to be returned in the GraphQL result.

TypeScript
1new LocalState({
2  resolvers: {
3    Query: {
4      localField: () => {
5        try {
6          throw new Error("Could not get localField");
7        } catch (e) {
8          console.log(e);
9        }
10      },
11    },
12  },
13});

Returned values

Apollo Client 4 now checks the return value of local resolvers to ensure it returns a value or null. undefined is no longer a valid value.

When undefined is returned from a local resolver, the value is set to null and a warning is logged to the console.

This example returns undefined due to the early return.

TypeScript
1new LocalState({
2  resolvers: {
3    Query: {
4      localField: () => {
5        if (someCondition) {
6          return;
7        }
8
9        return "value";
10      },
11    },
12  },
13});

This results in the following result:

TypeScript
1{
2  data: {
3    localField: null,
4  },
5};

And a warning is emitted to the console:

The 'Query.localField' resolver returned undefined instead of a value. This is likely a bug in the resolver. If you didn't mean to return a value, return null instead.

Instead, the local resolver should return null instead.

TypeScript
1if (someCondition) {
2  return;
3  return null;
4}

The exception is when running local resolvers to get values for exported variables for fields marked with the @export directive. For this case, you may return undefined to omit the variable from the outgoing request. If you need to know whether to return undefined or null, use the phase property provided to the context argument.

TypeScript
1new LocalState({
2  resolvers: {
3    Query: {
4      localField: (parent, args, { phase }) => {
5        // `phase` is "resolve" when resolving the field for the response
6        return phase === "resolve" ? null : undefined;
7        // `phase` is "exports" when getting variables for `@export` fields.
8        return phase === "exports" ? undefined : null;
9      },
10    },
11  },
12});

Enforced __typename

Apollo Client 4 now validates that a __typename property is returned from resolvers that return arrays or objects on non-scalar fields. If __typename is not included, an error is added to the errors array and the field value is set to null. This change ensures the object is cached properly to avoid bugs that might otherwise be difficult to track down.

If your local resolver returns an object or an array for a non-scalar field, make sure it includes the __typename field.

TypeScript
1const query = gql`
2  query {
3    localUser @client {
4      id
5    }
6  }
7`;
8
9new LocalState({
10  resolvers: {
11    Query: {
12      localUser: () => ({
13        __typename: "LocalUser",
14        // ...
15      }),
16    },
17  },
18});

Behavior changes with @export fields

Apollo Client 4 adds additional validation to fields and resolvers that use the @export directive before the request is sent to the server.

Variable definitions

Each @export field is now checked to ensure it can be associated with a variable definition in the GraphQL document. If the @export directive doesn't include the as argument, or the GraphQL document doesn't define a variable definition that matches the name provided to the as argument, a LocalStateError is thrown.

You will need to ensure the GraphQL document includes variable definitions for all @export fields.

TypeScript
1// This throws because "someVar" is not defined
2const query = gql`
3  query {
4    field @client @export(as: "someVar")
5  }
6`;
7
8// This is valid
9const query = gql`
10  query ($someVar: String) {
11    field @client @export(as: "someVar")
12  }
13`;
Required variables

Resolvers that return values for required variables are now checked to ensure the value isn't nullable. If a resolver returns null or undefined for a required variable, a LocalStateError is thrown. This would otherwise cause an error on the server.

Errors in local resolvers

Local resolvers are not allowed to throw errors when resolving values for variables to avoid ambiguity on how to handle the error. If an error is thrown from a local resolver, it is wrapped in a LocalStateError and rethrown.

Since local resolvers are allowed to throw errors when resolving field data, you can use the phase property provided to the context argument to determine whether to rethrow the error or return a different value.

TypeScript
1new LocalState({
2  resolvers: {
3    Query: {
4      localField: (parent, args, { phase }) => {
5        try {
6          throw new Error("Oops couldn't get local field");
7        } catch (error) {
8          // Omit the variable value in the request
9          if (phase === "exports") {
10            return;
11          }
12
13          // Rethrow the error to add it to the `errors` array
14          throw error;
15        }
16      },
17    },
18  },
19});
Handling Local state errors

Each validation error throws an instance of LocalStateError. You can check if an error is a result of a local state error by using LocalStateError.is method. The error instance provides additional information about the path in the GraphQL document that caused the error.

TypeScript
1import { LocalStateError } from "@apollo/client";
2
3// ...
4
5const { error } = useQuery(QUERY);
6
7if (LocalStateError.is(error)) {
8  console.log(error.path, error.message);
9}

Custom fragment matchers

Apollo Client 3 allowed for custom fragment matchers using the fragmentMatcher option provided to the ApolloClient constructor. This made it possible to add your own custom logic to match fragment spreads used with client field selection sets. Fragment matching is now part of the cache with the fragmentMatches API.

Apollo Client 4 removes the fragmentMatcher option and the associated setLocalStateFragmentMatcher method that allows you to set a fragment matcher after the client was initialized. Remove the use of these APIs.

TypeScript
1const client = new ApolloClient({
2  fragmentMatcher: (rootValue, typeCondition, context) => true,
3});
4
5client.setLocalStateFragmentMatcher(() => true);

If you're using InMemoryCache, you're all set. InMemoryCache implements fragmentMatches for you. We recommend checking your possibleTypes configuration to ensure it is up-to-date with your local schema.

If you're using a custom cache implementation, you will need to check if it meets the new requirements for custom cache implementations. LocalState requires that the cache implements the fragmentMatches API. If the custom cache does not implement fragmentMatches, an error is thrown.

A lot of utilities that were previously part of the @apollo/client/utilities and @apollo/client/testing packages have been removed. They were used for internal testing purposes and we are not using them anymore, so we removed them.

MockedProvider changes

Default "realistic" delay

If no delay is specified in mocks, MockLink now defaults to the realisticDelay function, which uses a random delay between 20 and 50ms to ensure tests handle loading states.

If you want to restore the previous behavior of "no delay", you can set it via

TypeScript
1MockLink.defaultOptions = {
2  delay: 0,
3};

The createMockClient and mockSingleLink utilities have been removed. Instead, you can now use the MockLink class directly to create a mock link and pass it into a new ApolloClient instance.

Bundling changes

exports field in package.json

Apollo Client 4 now uses the exports field in its package.json to define which files are available for import. This means you should now be able to use import { ApolloClient } from "@apollo/client" instead of import { ApolloClient } from "@apollo/client/index.js" or import { ApolloClient } from "@apollo/client/main.cjs".

New transpilation target, no shipped polyfills

Apollo Client is now transpiled to target a since 2023, node >= 20, not dead target. This means that Apollo Client can now use modern JavaScript features without downlevel transpilation, which will generally result in better performance and smaller bundle size. We also have stopped the practice of shipping polyfills.

Please note that we might bump the transpilation target to more modern targets in upcoming minor versions. See our versioning policy for more details on the supported environments.

If you are targeting older browsers or special environments, you might need to adjust your build configuration to transpile the Apollo Client library itself to a lower target, or polyfill the missing features yourself.

Development mode changes

Previously, you had to set a global __DEV__ variable to false to disable development mode. Now, development mode is primarily controlled by the development export condition of the package.json exports field. Most modern bundlers should now automatically pick correctly between the development and production version of Apollo Client based on your build environment.

If your build tooling does not support the development or production export condition, Apollo Client falls back to the previous behavior, meaning that you can still set the __DEV__ global variable to false to disable development mode in those cases.

Other notable breaking changes

New requirements for custom cache implementations

Custom cache implementations must now implement the fragmentMatches method, which is required for fragment matching in Apollo Client 4.

Feedback

Edit on GitHub

Ask Community