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/react
Better ESM support with
exports
field inpackage.json
Observable implementation now uses
rxjs
instead ofzen-observable
More 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 graphql rxjs
rxjs
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
,useQuery
updates
Changes affecting fewer users:
Link API changes - If you use custom links
Local state changes - If you use
@client
directivesTesting 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:
legacyEntrypoints
step:- Updates CommonJS import statements using the
.cjs
extension to their associated entry pointTypeScript1import { ApolloClient } from "@apollo/client/main.cjs"; 2import { ApolloClient } from "@apollo/client";
- Updates ESM import statements using the
.js
extension to their associated entry pointTypeScript1import { ApolloClient } from "@apollo/client/index.js"; 2import { ApolloClient } from "@apollo/client";
- Updates CommonJS import statements using the
imports
step:- 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
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" });
noteIf you use thesetContext
link, you will need to manually migrate to theSetContextLink
class because the order of arguments has changed. This is not automatically performed by the codemod. - Updates the usage of
from
,split
andconcat
functions from@apollo/client/link
to use the static methods on theApolloLink
class. 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.
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.TypeScript1import { Concast } from "@apollo/client"; 2import { Concast } from "@apollo/client/v4-migration";
noteAny runtime values exported from@apollo/client/v4-migration
will throw an error at runtime since their implementations do not exist.- Updates exports removed from Apollo Client to a special
clientSetup
step:- Moves
uri
,headers
andcredentials
to thelink
option and creates a newHttpLink
instanceTypeScript1import { 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
andversion
into aclientAwareness
optionTypeScript1new 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 newLocalState
instance, moves theresolvers
toLocalState
, and removes thetypeDefs
andfragmentMatcher
optionsTypeScript1import { 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 todevtools.enabled
TypeScript1new ApolloClient({ 2 connectToDevTools: true, 3 devtools: { 4 enabled: true, 5 }, 6});
- Renames
disableNetworkFetches
toprioritizeCacheValues
TypeScript1new 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 totrue
TypeScript1new 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 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
To run the codemod, use the following command:
npx @apollo/client-codemod-migrate-3-to-4 src
jscodeshift
with a preselected codemod.For more details on the available options, run the command using the --help
option.
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.
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
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:
npx @apollo/client-codemod-migrate-3-to-4 --codemod imports --codemod links src
The following codemods are available:
clientSetup
- Updates the options provided to theApolloClient
constructor to use their new counterpartslegacyEntrypoints
- 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()
becomesnew 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
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
ObservableQuery
instances 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});
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); // => undefined
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.
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
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.
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 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:
FetchResult
is nowApolloLink.Result
ApolloClientOptions
is nowApolloClient.Options
QueryOptions
is nowApolloClient.QueryOptions
ApolloQueryResult
is nowObservableQuery.Result
QueryHookOptions
is nowuseQuery.Options
QueryResult
is nowuseQuery.Result
LazyQueryResult
is 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});
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
Accept
header is nowapplication/graphql-response+json,application/json;q=0.9
for all outgoing requestsThe
application/graphql-response+json
media type is now supported and the client handles the response according to theapplication/graphql-response+json
behaviorThe
application/json
media type behaves according to theapplication/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 acontent-type
usingapplication/json
and returns a non-200 status codeThe client will now throw a
ServerError
when the server encodes using any othercontent-type
and 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:
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.
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
undefined
instead of a value. This is likely a bug in the resolver. If you didn't mean to return a value, returnnull
instead.
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.