Skip to content

Migrating v4 components ​

v4 shipped @vue/apollo-components, a set of renderless components built on @vue/apollo-option and its this.$apollo smart-query layer. v5 keeps the package and the element names, and rebuilds them on top of @vue/apollo-composable.

This page lists what moved.

No compat layer for components

@vue/apollo-composable/compat covers the composables only. The components are a from-scratch rewrite with no v4-signature shim, so the changes here have to be made before the app runs.

The changes are mechanical and mostly confined to templates, so a codebase-wide search for <ApolloQuery is a good way to scope the work.

Step 1: Dependencies ​

@vue/apollo-option is gone. The components now depend on the composables:

shell
npm uninstall @vue/apollo-option
npm install @apollo/client@^4.1 @vue/apollo-composable@next @vue/apollo-components@next graphql
shell
yarn remove @vue/apollo-option
yarn add @apollo/client@^4.1 @vue/apollo-composable@next @vue/apollo-components@next graphql
shell
pnpm remove @vue/apollo-option
pnpm add @apollo/client@^4.1 @vue/apollo-composable@next @vue/apollo-components@next graphql

Step 2: Client setup ​

v4 registered an ApolloProvider. v5 provides the client itself, the same way the composables do:

ts
// Before (v4)
import { createApolloProvider } from '@vue/apollo-option'

const apolloProvider = createApolloProvider({ defaultClient: apolloClient })

createApp(App)
  .use(apolloProvider)
  .mount('#app')
ts
// After (v5)
import { DefaultApolloClient } from '@vue/apollo-composable'

createApp(App)
  .provide(DefaultApolloClient, apolloClient)
  .mount('#app')

For several clients, provide ApolloClients instead. See Multiple Clients.

Step 3: Registration ​

v4 auto-installed from a global Vue, and its plugin was exported as ApolloProvider. v5 exports VueApolloComponents, and prefers direct imports:

ts
// Before (v4)
import { ApolloProvider } from '@vue/apollo-components'

app.use(ApolloProvider)
ts
// After (v5), global
import { VueApolloComponents } from '@vue/apollo-components'

app.use(VueApolloComponents)
vue
<!-- After (v5), preferred -->
<script setup lang="ts">
import { ApolloQuery } from '@vue/apollo-components'
</script>

Global registration loses the generic slot-prop types, so data in #data falls back to any. Import the components where you use them to keep them typed.

Step 4: The wrapper element is gone ​

Every v4 component rendered a <div> around its slot content, configurable with the tag prop. The v5 components render exactly what their slots return and nothing else.

template
<!-- v4: this produced <div><ul>...</ul></div> -->
<ApolloQuery :query="GetDogs">
  ...
</ApolloQuery>

Delete any tag prop, and check styling that relied on the wrapper. If you need one back, write it yourself:

template
<div class="panel">
  <ApolloQuery :query="GetDogs">
    ...
  </ApolloQuery>
</div>

Multiple roots

Because the components are renderless, an <ApolloQuery> that renders several elements makes its parent a multi-root component. That is fine in Vue 3, but attribute fallthrough stops working, so wrap it if the parent passes class or style down.

<ApolloQuery> ​

Props ​

v4v5
query (document or gql => document)query, a document only. Use a TypedDocumentNode to get typed slots.
variablesvariables, typed from the document
skipdisabled
fetchPolicySame name
pollIntervalSame name
debounce, throttle (default 0)Same names, no default. Absent means no delay.
clientIdSame name
update (transform the data)Removed. Transform inside the slot, or with a cache field policy.
notifyOnNetworkStatusChange, context, deep, prefetchThrough options, where the option still exists in Apollo Client v4
tagRemoved, see above
optionsoptions, now the whole useQuery.Options object

skip to disabled is a rename only; the meaning is identical. See Disabling queries.

Slots ​

v4 had one slot, handing you a result object to branch on yourself. v5 adds named slots that do the branching, and keeps a default slot for when you would rather do it yourself:

template
<!-- Before (v4) -->
<ApolloQuery v-slot="{ result: { data, loading, error } }" :query="GetDogs">
  <div v-if="loading">Loading...</div>
  <div v-else-if="error">{{ error.message }}</div>
  <ul v-else-if="data">
    <li v-for="dog in data.dogs" :key="dog.id">{{ dog.breed }}</li>
  </ul>
</ApolloQuery>
template
<!-- After (v5) -->
<ApolloQuery :query="GetDogs">
  <template #loading>
    Loading...
  </template>
  <template #error="{ error }">
    {{ error.message }}
  </template>
  <template #data="{ data }">
    <ul>
      <li v-for="dog in data.dogs" :key="dog.id">
        {{ dog.breed }}
      </li>
    </ul>
  </template>
</ApolloQuery>

#empty is new, and it is the last branch in the chain: a query that settles with no result, no error and nothing in flight renders it where v4 rendered nothing. Add the empty prop, a predicate over the result, to also catch results that arrived but count as empty, such as a zero-length list. A disabled query is the exception and still renders nothing at all.

The slot-prop names changed too:

v4 slot propv5
result.datadata in #data, never undefined there. result in the default slot.
result.loadingloading, in #data and in the default slot
result.errorerror, in #error, #data and the default slot. In #data it means the query failed but there is still something to show.
result.networkStatusnetworkStatus, default slot only
result.fullDataRemoved, along with the update prop it existed for
isLoadingloading
gqlErrorRemoved. Narrow with CombinedGraphQLErrors.is(error), see Error Handling.
timesRemoved
queryOn the exposed result, through a template ref

A straight port keeps the v-if chain and uses only the default slot, which is a valid v5 form. Moving to the named slots is worth doing though: data is non-nullable inside #data, so the optional chaining v4 needed disappears. Two ways to read the result covers both modes.

Events ​

v4v5
@loading="isLoading => ..."Removed. Read the loading slot prop.
@result="result => ...", receiving { data, loading, error, ... }@result="data => ...", receiving the data alone. @nextState gives the whole state.
@error="error => ..."Same name. The payload is an ErrorLike rather than a v3 ApolloError.

v5 also emits @completeResult, @partialResult and @streamingResult. See Event hooks.

Retaining results between variable changes ​

v4's <ApolloQuery> merged the previous data into the new result while loading, so a variables change never blanked the list. v5 does not do this unless asked:

template
<ApolloQuery :query="SearchProducts" :variables="{ term }" keepPreviousResult>

The retained result is flagged with the isPreviousResult slot prop, which v4 had no equivalent for, so a stale list can be styled as stale. See Keeping previous data.

<ApolloMutation> ​

Props ​

v4v5
mutation (document or gql => document)mutation, a document only
variablesvariables, typed from the document
clientIdSame name
optimisticResponse, update, refetchQueries, contextThrough options, typed as useMutation.Options
tagRemoved
template
<!-- Before (v4) -->
<ApolloMutation
  :mutation="AddTodo"
  :variables="{ text }"
  :refetchQueries="() => ['GetTodos']"
/>
template
<!-- After (v5) -->
<ApolloMutation
  :mutation="AddTodo"
  :variables="{ text }"
  :options="{ refetchQueries: ['GetTodos'] }"
/>

mutate() ​

<ApolloMutation>'s mutate slot prop now takes an options object rather than bare variables, matching useMutation:

template
<!-- Before (v4) -->
<button @click="mutate({ text })">Add</button>
template
<!-- After (v5) -->
<button @click="mutate({ variables: { text } })">Add</button>

Calling it with no arguments still uses the variables prop, as in v4.

Slot props and events ​

mutate, loading and error are unchanged. gqlError is gone, for the same reason as on <ApolloQuery>. v5 adds called, result and reset.

@done and @error keep their names. @loading is gone; use the loading slot prop.

v4's mutate caught every error and resolved with undefined. v5's rejects instead: throws defaults to 'auto', and the component bridges @error to useMutation's onError only while the parent is actually listening, so with nothing bound there is no handler and the promise rejects. A v4 call site that never expected a rejection needs one of two things: bind @error, which makes mutate() resolve again and delivers the failure to the event, or set :options="{ throws: 'never' }" and read the error slot prop. See Error throwing behavior.

<ApolloSubscribeToMore> ​

<ApolloSubscribeToMore> keeps its name and its placement: it still takes document, variables and updateQuery, and still has to sit inside an <ApolloQuery>.

There is one addition: a context prop, passed to the link chain for this subscription. v4 had no equivalent, so there is nothing to migrate; it is there for headers and link state that only the subscription needs.

One difference:

  • Changing updateQuery alone does not re-subscribe. Only document, variables and context do.
template
<ApolloQuery :query="GetMessages" :variables="{ channelId }">
  <ApolloSubscribeToMore
    :document="OnNewMessage"
    :variables="{ channelId }"
    :updateQuery="mergeMessage"
  />
  <template #data="{ data }">
    ...
  </template>
</ApolloQuery>

Note that it now lives in the default slot rather than beside a single slot's content. The default slot renders in both modes.

New in v5 ​

Two components have no v4 equivalent:

What has no component form ​

  • Lazy queries. See Lazy Queries; disabled covers the "wait for variables" case.
  • Suspense. See Suspense.
  • Aggregate loading counters. useQueryLoading and friends are scoped per component instance, so they cannot see a child <ApolloQuery>. See Loading States.

The two APIs share one client and one cache, so mixing them is expected. Reach for <script setup> in the components that need it and keep the templates declarative everywhere else.

Next steps ​

Released under the MIT License.