Upgrading an issue-detail component from Apollo Client 3 to 4, what must change in a useQuery call that imports from @apollo/client and uses skip and onCompleted?
answer
- a new home for the hooks
- query hooks lost their callbacks
- a token instead of a boolean
- loading now flips on refetch
- skipping no longer clears data
basics
~20 sIn Apollo Client 4, import useQuery from @apollo/client/react and move onCompleted logic into render or an effect on data, because query hooks lost that callback. Replace skip with skipToken, deprecated since 4.3, and expect loading during refetch.
solid answer
~40 sFour things move. The import becomes `@apollo/client/react`, because `@apollo/client` no longer exports React APIs. `onCompleted` and `onError` were removed from `useQuery` and `useLazyQuery`. Derive UI from `data` and `error` during render, and run a genuine side effect in an effect keyed on `data`. `skip: !issueId` becomes `useQuery(ISSUE, issueId ? { variables: { id: issueId } } : skipToken)`, which drops the `id: issueId!` cast. `skip` still works but has been deprecated since 4.3. Behaviour changes too. `notifyOnNetworkStatusChange` defaults to `true`, so `loading` turns true during `refetch` and polling. Switching to skipped now keeps the last `data` instead of clearing it. Changing most options between renders no longer refetches. `useMutation` keeps its `onCompleted` and `onError` options.
code
tsx · 24 linesimport { useEffect } from "react";
import { gql, type TypedDocumentNode } from "@apollo/client";
import { skipToken, useQuery } from "@apollo/client/react";
const ISSUE: TypedDocumentNode<
{ issue: { id: string; title: string; status: string } | null },
{ id: string }
> = gql`
query Issue($id: ID!) {
issue(id: $id) { id title status }
}
`;
export function IssueDetail({ issueId }: { issueId?: string }) {
const { data, error } = useQuery(ISSUE, issueId ? { variables: { id: issueId } } : skipToken);
useEffect(() => {
if (data?.issue) document.title = data.issue.title;
}, [data]);
if (error) return <p role="alert">{error.message}</p>;
if (!data?.issue) return <p>{issueId ? "Loading issue..." : "Select an issue"}</p>;
return <h1>{data.issue.title}</h1>;
}go deeper
Recall that Apollo Client 4 hooks import from @apollo/client/react and that useQuery no longer takes onCompleted or onError.
Rewrite a skip-with-cast call using skipToken, and replace onCompleted with render logic or an effect. Explain the loading changes caused by notifyOnNetworkStatusChange.
Anticipate the behaviour changes that break tests, such as extra loading renders, data kept when skipped, and lazier option changes, and sequence the migration so each step stays green.
Plan the upgrade across many teams: codemod first, a policy against reverting defaults globally, and a deadline for replacing the deprecated skip option before a future release removes it.
## What changed around useQuery Apollo Client 4 is a major release, and several changes land on an ordinary `useQuery` call. Some are compile-time, where the old code no longer builds. Others are behavioural, where it builds but renders differently. For an issue-detail component the relevant ones are: | Area | Apollo Client 3 | Apollo Client 4 | What to do | |---|---|---|---| | Import path | `import { useQuery } from "@apollo/client"` | hooks only in `@apollo/client/react` | change the import | | Callbacks | `onCompleted`, `onError` on `useQuery` | removed from `useQuery` and `useLazyQuery` | render from `data`/`error`, or use an effect | | Skipping | `skip: !issueId` plus a non-null cast | `skipToken` (since 4.0.4); `skip` deprecated in 4.3 | pass `skipToken` instead of options | | Refresh re-renders | `notifyOnNetworkStatusChange` default `false` | default `true` | handle `loading` beside `data` | | Data when skipped | switching to skipped cleared `data` | the last `data` is kept | do not rely on `data` vanishing | | Changing options | most option changes triggered a reobserve that could fetch | only `query`, `variables`, `skip`, or a switch to or from `standby` do | expect new options to apply on the next fetch | Other things around the hook also moved. `ApolloError` became error classes such as `CombinedGraphQLErrors`, the client now requires an explicit `link`, and the links became classes. Those belong to error handling and client setup, not to this component. ## Replacing onCompleted The migration guide calls the old callbacks ambiguous and easy to misuse, and 4.0 removed them from the query hooks. The replacements depend on what the callback did: - **It set local state from the result.** Delete the state and read `data` directly during render. Derived values do not need copying. - **It performed a side effect**, such as setting `document.title` or sending an analytics event. Put it in a React effect whose dependency is `data`, so it runs when the data actually changes. - **It handled an error**, such as showing a toast. Read `error` from the result. For app-wide handling, an `ErrorLink` in the link chain sees every failed operation. - **It chained a follow-up request.** With `useLazyQuery`, `await` the promise returned by `execute` and continue there. `useMutation` is different. Its `onCompleted` and `onError` options still exist in 4.x, and code that uses them there can stay. ## skipToken instead of skip With `skip`, TypeScript still demands every required variable, so code ends up with `variables: { id: issueId! }` and a `skip: !issueId` guard that can drift apart. `skipToken`, imported from `@apollo/client/react`, replaces the whole options object: 1. When `issueId` is defined, pass `{ variables: { id: issueId } }`, and TypeScript checks the variables as usual. 2. When it is not, pass `skipToken`, and the query waits in `standby` without sending anything. 3. The result for a query skipped from the start is `loading: false`, `data: undefined`, `dataState: "empty"`, so the component must handle "no data and not loading". `skip` still works in 4.3 but is marked deprecated, and `skipToken` reached `useQuery` in 4.0.4. ## Behaviour changes that show up in tests 1. **Extra loading renders.** Because `notifyOnNetworkStatusChange` now defaults to `true`, `refetch()` and polling re-render with `loading: true` and `networkStatus` `refetch` or `poll`. Snapshot tests and "spinner then content" assertions change. The old default can be restored through `defaultOptions.watchQuery`, but handling `loading` beside `data` is the better fix. 2. **Data survives skipping.** A component that switched to skipped used to see `data` go `undefined`. Now it keeps the last result, so "clear the form when skipped" logic needs its own condition. 3. **Option changes are lazier.** Changing `fetchPolicy` between renders no longer fires a request. It applies at the next fetch or cache update. 4. **The deprecated `errors` field is gone** from the result. Read the single `error`. ## A migration order that stays green - Run the official codemod for import paths and renamed types, then fix what it cannot reach. - Remove `onCompleted` and `onError` from query hooks one by one, choosing render, effect or `execute` per case. - Convert `skip` to `skipToken` where variables are conditional. - Re-run the component tests and adjust loading assertions, rather than switching `notifyOnNetworkStatusChange` off globally.
- Why is skipToken safer than skip for TypeScript?With `skip`, the options object must still satisfy the query's variable types, so a possibly undefined id gets a `!` cast or a dummy default. If the skip condition later changes, the query can run with a bad id and TypeScript cannot warn. `skipToken` replaces the whole options object, so real variables are passed only where they exist.
- What replaces useQuery's onError for showing a toast when an issue fails to load?Read `error` from the hook's result. Render it inline, or show the toast from an effect that depends on `error`. For errors that every screen treats the same way, such as an expired session, handle them once in an `ErrorLink` in the client's link chain instead of in each component.
saying these in an interview costs you the question
- onCompleted was removed from useMutation as well as useQuery.
- skip was removed in Apollo Client 4 and now throws.
- Hooks can still be imported from @apollo/client in version 4.
- Changing fetchPolicy between renders immediately triggers a new request in 4.x.
- Skipping a query in 4.x resets data to undefined.