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.
What’s new in 4.0
Framework & Bundle Improvements
Framework-agnostic core with React exports moved to
@apollo/client/reactBetter ESM support with
exportsfield inpackage.jsonObservable implementation now uses
rxjsinstead ofzen-observableMore features are opt-in to reduce bundle size when not used
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
Install Apollo Client 4 along with its peer dependencies with the following command:
npm install @apollo/client@latest graphql rxjsrxjs is a new peer dependency with Apollo Client 4.0.Recommended migration approach
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:
Core API changes - Query behavior changes
Error handling updates - New error classes and patterns
TypeScript improvements - New type locations and patterns
Changes affecting React users:
React hook changes -
useLazyQuery,useMutation,useQueryupdates
Changes affecting fewer users:
Link API changes - If you use custom links
Local state changes - If you use
@clientdirectivesTesting changes - MockedProvider updates
Bundling changes - New export patterns and transpilation
Development mode changes - Automatic detection
Codemod
To ease the migration process, we have provided a codemod that automatically updates your codebase.
This codemod runs modifications in the following order:
legacyEntrypointsstep:- Updates CommonJS import statements using the
.cjsextension to their associated entry pointTypeScript1import { ApolloClient } from "@apollo/client/main.cjs"; 2import { ApolloClient } from "@apollo/client"; - Updates ESM import statements using the
.jsextension to their associated entry pointTypeScript1import { ApolloClient } from "@apollo/client/index.js"; 2import { ApolloClient } from "@apollo/client";
- Updates CommonJS import statements using the
importsstep:- Updates imports that have moved aroundTypeScript
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>();
- Updates imports that have moved around
linksstep:- 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" });noteIf you use thesetContextlink, you will need to manually migrate to theSetContextLinkclass because the order of arguments has changed. This is not automatically performed by the codemod. - Updates the usage of
from,splitandconcatfunctions from@apollo/client/linkto use the static methods on theApolloLinkclass. For example:TypeScript1import { from } from "@apollo/client"; 2import { ApolloLink } from "@apollo/client"; 3// ... 4const link = from([a, b, c]); 5const link = ApolloLink.from([a, b, c]);
- Updates the usage of Apollo-provided links to their associated class implementation.
removalsstep:- Updates exports removed from Apollo Client to a special
@apollo/client/v4-migrationentrypoint. This is a type-only entrypoint that contains doc blocks with migration instructions for each removed item.TypeScript1import { Concast } from "@apollo/client"; 2import { Concast } from "@apollo/client/v4-migration";
noteAny runtime values exported from@apollo/client/v4-migrationwill throw an error at runtime since their implementations do not exist.- Updates exports removed from Apollo Client to a special
clientSetupstep:- Moves
uri,headersandcredentialsto thelinkoption and creates a newHttpLinkinstanceTypeScript1import { 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
nameandversioninto aclientAwarenessoptionTypeScript1new 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
localStateoption with a newLocalStateinstance, moves theresolverstoLocalState, and removes thetypeDefsandfragmentMatcheroptionsTypeScript1import { 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
connectToDevToolsoption todevtools.enabledTypeScript1new ApolloClient({ 2 connectToDevTools: true, 3 devtools: { 4 enabled: true, 5 }, 6}); - Renames
disableNetworkFetchestoprioritizeCacheValuesTypeScript1new ApolloClient({ 2 disableNetworkFetches: true, 3 prioritizeCacheValues: true, 4}); - Adds a template for global type augmentation to re-enable data masking types when the
dataMaskingoption is set totrueTypeScript1new 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
incrementalHandleroption and adds a template for global type augmentation to accordingly type network responses in custom linksTypeScript1import { 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*/
- Moves
Running the codemod
require. See the table in the Node.js documentation for the versions that support this feature (you might need to expand the "History" section to see the table.)To run the codemod, use the following command:
npx @apollo/client-codemod-migrate-3-to-4 srcjscodeshift with a preselected codemod.src directory with the codemod. Replace src with the file pattern applicable to your file structure if it differs.For more details on the available options, run the command using the --help option.
npx @apollo/client-codemod-migrate-3-to-4 --helpUsing 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.
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 srcRunning 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:
npx @apollo/client-codemod-migrate-3-to-4 --codemod imports --codemod links srcThe following codemods are available:
clientSetup- Updates the options provided to theApolloClientconstructor to use their new counterpartslegacyEntrypoints- Renames import statements that target.jsor.cjsimports to their associated entry point (e.g.@apollo/client/main.cjsbecomes@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()becomesnew HttpLink()).removals- Updates import statements that contain removed features to a special@apollo/client/v4-migrationentry point.
Updating imports
Move from manual CJS/ESM imports to exports
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 point | New 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 |
| Previous entry point | Reason |
|---|---|
@apollo/client/react/components | The render prop components were already deprecated in Apollo Client 3.x and have been removed in 4. |
@apollo/client/react/hoc | The higher order components were already deprecated in Apollo Client 3.x and have been removed in 4. |
@apollo/client/react/parser | This module was an implementation detail of the render prop components and HOCs. |
@apollo/client/testing/experimental | This is available as @apollo/graphql-testing-library |
@apollo/client/utilities/globals | This was an implementation detail and has been removed. Some of the exports are now available in other entry points. |
@apollo/client/utilities/subscriptions/urql | This is supported natively by urql and is no longer included. |
| Previous entry point | New entry point |
|---|---|
| Previous import name | New import name |
@apollo/client | (unchanged) |
ApolloClientOptions | ApolloClient.Options |
DefaultOptions | ApolloClient.DefaultOptions |
DevtoolsOptions | ApolloClient.DevtoolsOptions |
MutateResult | ApolloClient.MutateResult |
MutationOptions | ApolloClient.MutateOptions |
QueryOptions | ApolloClient.QueryOptions |
RefetchQueriesOptions | ApolloClient.RefetchQueriesOptions |
RefetchQueriesResult | ApolloClient.RefetchQueriesResult |
SubscriptionOptions | ApolloClient.SubscribeOptions |
WatchQueryOptions | ApolloClient.WatchQueryOptions |
ApolloQueryResult | ObservableQuery.Result |
FetchMoreOptions | ObservableQuery.FetchMoreOptions |
SubscribeToMoreOptions | ObservableQuery.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) |
BackgroundQueryHookFetchPolicy | useBackgroundQuery.FetchPolicy |
BackgroundQueryHookOptions | useBackgroundQuery.Options |
BaseSubscriptionOptions | useSubscription.Options |
Context | (unchanged) |
LazyQueryExecFunction | useLazyQuery.ExecFunction |
LazyQueryHookExecOptions | useLazyQuery.ExecOptions |
LazyQueryHookOptions | useLazyQuery.Options |
LazyQueryResult | useLazyQuery.Result |
LazyQueryResultTuple | useLazyQuery.ResultTuple |
LoadableQueryHookFetchPolicy | useLoadableQuery.FetchPolicy |
LoadableQueryHookOptions | useLoadableQuery.Options |
LoadQueryFunction | useLoadableQuery.LoadQueryFunction |
MutationFunction | (unchanged) |
MutationFunctionOptions | useMutation.MutationFunctionOptions |
MutationHookOptions | useMutation.Options |
MutationResult | useMutation.Result |
MutationTuple | useMutation.ResultTuple |
NoInfer | (unchanged) |
OnDataOptions | useSubscription.OnDataOptions |
OnSubscriptionDataOptions | useSubscription.OnSubscriptionDataOptions |
PreloadedQueryRef | (unchanged) |
PreloadQueryFetchPolicy | (unchanged) |
PreloadQueryFunction | (unchanged) |
PreloadQueryOptions | (unchanged) |
QueryFunctionOptions | useQuery.Options |
QueryHookOptions | useQuery.Options |
QueryRef | (unchanged) |
QueryReference | QueryRef |
QueryResult | useQuery.Result |
QueryTuple | useLazyQuery.ResultTuple |
SkipToken | (unchanged) |
SubscriptionDataOptions | (unchanged) |
SubscriptionHookOptions | useSubscription.Options |
SubscriptionResult | useSubscription.Result |
SuspenseQueryHookFetchPolicy | useSuspenseQuery.FetchPolicy |
SuspenseQueryHookOptions | useSuspenseQuery.Options |
UseBackgroundQueryResult | useBackgroundQuery.Result |
UseFragmentOptions | useFragment.Options |
UseFragmentResult | useFragment.Result |
UseLoadableQueryResult | useLoadableQuery.Result |
UseQueryRefHandlersResult | useQueryRefHandlers.Result |
UseReadQueryResult | useReadQuery.Result |
UseSuspenseFragmentOptions | useSuspenseFragment.Options |
UseSuspenseFragmentResult | useSuspenseFragment.Result |
UseSuspenseQueryResult | useSuspenseQuery.Result |
VariablesOption | (unchanged) |
@apollo/client/cache | (unchanged) |
WatchFragmentOptions | ApolloCache.WatchFragmentOptions |
WatchFragmentResult | ApolloCache.WatchFragmentResult |
@apollo/client/link | @apollo/client/incremental |
ExecutionPatchIncrementalResult | Defer20220824Handler.SubsequentResult |
ExecutionPatchInitialResult | Defer20220824Handler.InitialResult |
ExecutionPatchResult | Defer20220824Handler.Chunk |
IncrementalPayload | Defer20220824Handler.IncrementalDeferPayload |
Path | Incremental.Path |
@apollo/client/link | (unchanged) |
FetchResult | ApolloLink.Result |
GraphQLRequest | ApolloLink.Request |
NextLink | ApolloLink.ForwardFunction |
Operation | ApolloLink.Operation |
RequestHandler | ApolloLink.RequestHandler |
@apollo/client/link | graphql |
SingleExecutionResult | FormattedExecutionResult |
@apollo/client/link/batch | (unchanged) |
BatchHandler | BatchLink.BatchHandler |
@apollo/client/link/context | (unchanged) |
ContextSetter | SetContextLink.LegacyContextSetter |
@apollo/client/link/error | (unchanged) |
ErrorHandler | ErrorLink.ErrorHandler |
ErrorResponse | ErrorLink.ErrorHandlerOptions |
@apollo/client/link/http | @apollo/client/errors |
ServerParseError | (unchanged) |
@apollo/client/link/persisted-queries | (unchanged) |
ErrorResponse | PersistedQueryLink.DisableFunctionOptions |
@apollo/client/link/remove-typename | (unchanged) |
RemoveTypenameFromVariablesOptions | RemoveTypenameFromVariablesLink.Options |
@apollo/client/link/utils | @apollo/client/errors |
ServerError | (unchanged) |
@apollo/client/link/ws | (unchanged) |
WebSocketParams | WebSocketLink.Configuration |
@apollo/client/react | @apollo/client |
Context | DefaultContext |
@apollo/client/react | (unchanged) |
QueryReference | QueryRef |
ApolloProviderProps | ApolloProvider.Props |
BackgroundQueryHookFetchPolicy | useBackgroundQuery.FetchPolicy |
BackgroundQueryHookOptions | useBackgroundQuery.Options |
UseBackgroundQueryResult | useBackgroundQuery.Result |
LazyQueryExecFunction | useLazyQuery.ExecFunction |
LazyQueryHookExecOptions | useLazyQuery.ExecOptions |
LazyQueryHookOptions | useLazyQuery.Options |
LazyQueryResult | useLazyQuery.Result |
LazyQueryResultTuple | useLazyQuery.ResultTuple |
QueryTuple | useLazyQuery.ResultTuple |
LoadableQueryFetchPolicy | useLoadableQuery.FetchPolicy |
LoadableQueryHookFetchPolicy | useLoadableQuery.FetchPolicy |
LoadableQueryHookOptions | useLoadableQuery.Options |
LoadQueryFunction | useLoadableQuery.LoadQueryFunction |
UseLoadableQueryResult | useLoadableQuery.Result |
MutationFunctionOptions | useMutation.MutationFunctionOptions |
MutationHookOptions | useMutation.Options |
MutationResult | useMutation.Result |
MutationTuple | useMutation.ResultTuple |
BaseSubscriptionOptions | useSubscription.Options |
OnDataOptions | useSubscription.OnDataOptions |
OnSubscriptionDataOptions | useSubscription.OnSubscriptionDataOptions |
SubscriptionHookOptions | useSubscription.Options |
SubscriptionResult | useSubscription.Result |
QueryFunctionOptions | useQuery.Options |
QueryHookOptions | useQuery.Options |
QueryResult | useQuery.Result |
SuspenseQueryHookFetchPolicy | useSuspenseQuery.FetchPolicy |
SuspenseQueryHookOptions | useSuspenseQuery.Options |
UseSuspenseQueryResult | useSuspenseQuery.Result |
UseQueryRefHandlersResult | useQueryRefHandlers.Result |
UseFragmentOptions | useFragment.Options |
UseFragmentResult | useFragment.Result |
UseReadQueryResult | useReadQuery.Result |
UseSuspenseFragmentOptions | useSuspenseFragment.Options |
UseSuspenseFragmentResult | useSuspenseFragment.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) |
MockedRequest | MockLink.MockedRequest |
MockedResponse | MockLink.MockedResponse |
MockLinkOptions | MockLink.Options |
ResultFunction | MockLink.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.
Explicitly provide HttpLink
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.
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.
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.
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.
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.
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.
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.
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.
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:
1import { Defer20220824Handler } from "@apollo/client/incremental";
2
3declare module "@apollo/client" {
4 export interface TypeOverrides extends Defer20220824Handler.TypeOverrides {}
5}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:
1import { GraphQLCodegenDataMasking } from "@apollo/client/masking";
2
3declare module "@apollo/client" {
4 export interface TypeOverrides
5 extends GraphQLCodegenDataMasking.TypeOverrides {}
6}TypeOverrides of different kinds. For example, you can combine the data masking types with the Defer20220824Handler type overrides from the previous section.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.
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.
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
ObservableQueryinstances that don't have at least one subscriber are not tracked or accessible through the client
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.
1useQuery(QUERY, {
2 canonizeResults: true,
3});Subscription deduplication
Subscriptions are now deduplicated when queryDeduplication is enabled (the default). Subscriptions that qualify for deduplication attach to the same connection as a previously connected subscription.
To disable query deduplication, provide queryDeduplication: false to the context option when creating the subscription.
1// with React's `useSubscription`
2useSubscription(SUBSCRIPTION, {
3 context: { queryDeduplication: false },
4});
5
6// with the core API
7const observable = client.subscribe({
8 query: SUBSCRIPTION,
9 context: { queryDeduplication: false },
10});If you rely on the initial value to read data from the subscription, we recommend disabling query deduplication so that the subscription creates a new connection.
ObservableQuery no longer inherits from Observable
ObservableQuery instances returned from client.watchQuery no longer inherit from Observable. This might cause TypeScript errors when using ObservableQuery with some RxJS utilities that expect Observable instances, such as firstValueFrom.
Convert ObservableQuery to an Observable using the from function.
1import { from } from "rxjs";
2
3const observable = from(observableQuery);
4// ^? Observable<ObservableQuery.Result<MaybeMasked<TData>>>ObservableQuery implements the Subscribable interface, so you can call subscribe directly without converting to an Observable. It also implements the pipe function, so you can chain operators.Use
from only for functions that require an Observable instance.Removal of Observable utilities
Apollo Client 3 included several utilities for working with Observable instances from the zen-observable library. Apollo Client 4 removes these utilities in favor of RxJS's operators.
fromError
Use the throwError function instead.
1const observable = fromError(new Error("Oops"));
2const observable = throwError(() => {
3 return new Error("Oops");
4});fromPromise
Use the from function instead.
1const observable = fromPromise(promise);
2const observable = from(promise);toPromise
Use the firstValueFrom function instead.
1const promise = toPromise(observable);
2const promise = firstValueFrom(observable);New error handling
Error handling in Apollo Client 4 has changed significantly to be more predictable and intuitive.
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.
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.
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.
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.
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.
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.".
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.
1const { data, error } = await client.query({ query, errorPolicy: "all" });
2
3console.log(data); // => undefined
4console.log(error); // => new Error("...")data to undefined.errorPolicy: ignore
The promise is resolved and the error is ignored.
1const { data, error } = await client.query({ query, errorPolicy: "ignore" });
2
3console.log(data); // => undefined
4console.log(error); // => undefineddata 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.
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.
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.
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.
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:
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.
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.
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.
1function MyComponent() {
2 const [execute, { data }] = useLazyQuery(QUERY);
3 execute();
4
5 const { data } = useQuery(QUERY);
6
7 return /* ... */;
8}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.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}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.
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.
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
executefunction 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.
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}.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..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.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 promise resolution with errors for the execute function
The promise returned from the execute function is now consistently resolved or rejected based on your configured errorPolicy when errors are returned. With an errorPolicy of none, the promise always rejects. With an errorPolicy of all, the promise always resolves and sets the error property on the result. With an errorPolicy of ignore, the promise always resolves and discards the errors.
Previously, Apollo Client 3 resolved the promise when GraphQL errors were returned when using an errorPolicy of none. Network errors always caused the promise to reject, regardless of your configured error policy. This change unifies the error behavior across all error types to ensure errors are handled consistently.
To migrate code that uses an errorPolicy of none, wrap the call to execute in a try/catch block and use CombinedGraphQLErrors.is(error) to check for GraphQL errors.
1const [execute] = useLazyQuery(QUERY, {
2 errorPolicy: "none",
3});
4
5async function onUserInteraction() {
6 const { data, error } = await execute({ variables });
7 if (error.graphQLErrors) {
8 // handle GraphQL errors
9 }
10
11 try {
12 const { data } = await execute({ variables });
13 } catch (error) {
14 if (CombinedGraphQLErrors.is(error)) {
15 // handle GraphQL errors
16 }
17 }
18}To migrate code that uses an errorPolicy of all, read the error from the error property returned in the result. You can optionally remove any try/catch statements because the promise resolves for all error types.
1const [execute] = useLazyQuery(QUERY, {
2 errorPolicy: "all",
3});
4
5async function onUserInteraction() {
6 try {
7 const { data } = await execute({ variables });
8 } catch (error) {
9 if (error.graphQLErrors) {
10 // handle GraphQL errors
11 }
12 }
13
14 const { data, error } = await execute({ variables });
15 if (CombinedGraphQLErrors.is(error)) {
16 // handle GraphQL errors
17 }
18}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.
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.
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.
1function loader() {
2 const queryRef = preloadQuery(QUERY, options);
3
4 return queryRef.toPromise();
5 return preloadQuery.toPromise(queryRef);
6}Recommended: Avoid using generated hooks
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 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:
FetchResultis nowApolloLink.ResultApolloClientOptionsis nowApolloClient.OptionsQueryOptionsis nowApolloClient.QueryOptionsApolloQueryResultis nowObservableQuery.ResultQueryHookOptionsis nowuseQuery.OptionsQueryResultis nowuseQuery.ResultLazyQueryResultis nowuseLazyQuery.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.
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.
1new ApolloClient<any>({
2new ApolloClient({
3 // ...
4});Removal of <TSerialized> generic argument
The TSerialized generic argument has been removed from the ApolloCache abstract class. Most users set this type to any which provided little benefit for type safety. APIs that previously relied on TSerialized are now set to unknown.
To migrate, remove the TSerialized generic argument when referencing ApolloCache.
1const cache: ApolloCache<any> = ...
2const cache: ApolloCache = ...Apollo Link changes
New class-based links
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 creator | New link class |
|---|---|
setContext | SetContextLink |
onError | ErrorLink |
createHttpLink | HttpLink |
createPersistedQueryLink | PersistedQueryLink |
removeTypenameFromVariables | RemoveTypenameFromVariablesLink |
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.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.
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.
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.
1import { ApolloLink } from "@apollo/client/link";
2
3ApolloLink.concat(firstLink, secondLink);
4ApolloLink.from([firstLink, secondLink]);from,concat,split static and instance methods now require ApolloLink instances
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.
1ApolloLink.from(
2 (operation, forward) => {
3 /*...*/
4 },
5 new ApolloLink((operation, forward) => {
6 /*...*/
7 }),
8 nextLink
9);Changes to ErrorLink
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.
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});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.
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:
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.
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.
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.
1import { execute } from "@apollo/client";
2
3const client = new ApolloClient({
4 // ...
5});
6
7execute(
8 link,
9 { query /* ... */ },
10 { client }
11);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.
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
Acceptheader is nowapplication/graphql-response+json,application/json;q=0.9for all outgoing requestsThe
application/graphql-response+jsonmedia type is now supported and the client handles the response according to theapplication/graphql-response+jsonbehaviorThe
application/jsonmedia type behaves according to theapplication/jsonbehavior
The client now does the following when application/json media type is returned.
The client will throw a
ServerErrorwhen the server encodes acontent-typeusingapplication/jsonand returns a non-200 status codeThe client will now throw a
ServerErrorwhen the server encodes using any othercontent-typeand returns a non-200 status code
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.
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.
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:
addResolversgetResolverssetResolvers
If you add resolvers after the initialization of the client, you may continue to do so with your LocalState instance using the addResolvers method.
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.
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:
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.
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.
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:
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.
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.
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:
1{
2 data: {
3 localField: null,
4 },
5};And a warning is emitted to the console:
The 'Query.localField' resolver returned
undefinedinstead of a value. This is likely a bug in the resolver. If you didn't mean to return a value, returnnullinstead.
Instead, the local resolver should return null instead.
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.
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.
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.
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.
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.
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.
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.
Testing-related changes
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
1MockLink.defaultOptions = {
2 delay: 0,
3};Removed createMockClient and mockSingleLink
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.