+
+
+
diff --git a/packages/docs/.vitepress/theme/composables/useApiFlavor.ts b/packages/docs/.vitepress/theme/composables/useApiFlavor.ts
new file mode 100644
index 00000000..8c5467d7
--- /dev/null
+++ b/packages/docs/.vitepress/theme/composables/useApiFlavor.ts
@@ -0,0 +1,28 @@
+import type { ApiFlavor } from '../../apiFlavors.ts'
+import { createSharedComposable, useLocalStorage } from '@vueuse/core'
+import { watch } from 'vue'
+import { API_FLAVORS, DEFAULT_FLAVOR, isApiFlavor, STORAGE_KEY } from '../../apiFlavors.ts'
+
+/** Nothing rendered depends on this. It drives `aria-checked` and the click handler. */
+function useApiFlavorState() {
+ const flavor = useLocalStorage(STORAGE_KEY, DEFAULT_FLAVOR, {
+ // Read after hydration, so the first client render matches the server's default.
+ initOnMounted: true,
+ listenToStorageChanges: false,
+ serializer: {
+ read: raw => (isApiFlavor(raw) ? raw : DEFAULT_FLAVOR),
+ write: value => value,
+ },
+ })
+
+ // Deliberately not `immediate`: the `head` script already applied the stored flavor
+ watch(flavor, (value) => {
+ for (const { value: candidate } of API_FLAVORS) {
+ document.documentElement.classList.toggle(`api-pref-${candidate}`, candidate === value)
+ }
+ })
+
+ return flavor
+}
+
+export const useApiFlavor = createSharedComposable(useApiFlavorState)
diff --git a/packages/docs/.vitepress/theme/index.ts b/packages/docs/.vitepress/theme/index.ts
index f1ced207..e02a6e8c 100644
--- a/packages/docs/.vitepress/theme/index.ts
+++ b/packages/docs/.vitepress/theme/index.ts
@@ -1,6 +1,8 @@
import type { EnhanceAppContext } from 'vitepress'
import TwoslashFloatingVue from '@shikijs/vitepress-twoslash/client'
import Theme from 'vitepress/theme'
+import { h } from 'vue'
+import ApiPreference from './components/ApiPreference.vue'
import SponsorButton from './components/SponsorButton.vue'
import '@shikijs/vitepress-twoslash/style.css'
@@ -8,6 +10,9 @@ import './styles/index.css'
export default {
extends: Theme,
+ Layout: () => h(Theme.Layout, null, {
+ 'sidebar-nav-before': () => h(ApiPreference),
+ }),
enhanceApp({ app }: EnhanceAppContext) {
app.use(TwoslashFloatingVue)
app.component('SponsorButton', SponsorButton)
diff --git a/packages/docs/.vitepress/theme/styles/api-flavor.css b/packages/docs/.vitepress/theme/styles/api-flavor.css
new file mode 100644
index 00000000..e176619c
--- /dev/null
+++ b/packages/docs/.vitepress/theme/styles/api-flavor.css
@@ -0,0 +1,70 @@
+/*
+ * Which flavor is visible is decided by the `api-pref-*` class a `head` script puts on
+ * before first paint. Those per-flavor rules are generated from `apiFlavors.ts` in
+ * `config.ts`, so adding a flavor needs no change here.
+ */
+
+.api-flavor {
+ display: none;
+
+ & > :first-child {
+ margin-top: 0;
+ }
+}
+
+.api-preference {
+ margin-bottom: 16px;
+ padding-top: 16px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid var(--vp-c-divider);
+}
+
+.api-preference__group {
+ position: relative;
+ display: grid;
+ padding: 3px;
+ border-radius: 8px;
+ background-color: var(--vp-c-default-soft);
+
+ & .api-preference__thumb {
+ position: absolute;
+ top: 3px;
+ right: 3px;
+ left: 3px;
+ height: calc((100% - 6px) / var(--api-flavor-count, 2));
+ border-radius: 6px;
+ background-color: var(--vp-c-bg);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.09);
+ transform: translateY(calc(100% * var(--api-flavor-index, 0)));
+ transition: transform 0.25s ease;
+
+ @media (prefers-reduced-motion: reduce) {
+ transition: none;
+ }
+ }
+
+ & .api-preference__option {
+ position: relative;
+ padding: 5px 10px;
+ border: 0;
+ border-radius: 6px;
+ background-color: transparent;
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 20px;
+ text-align: left;
+ white-space: nowrap;
+ color: var(--vp-c-text-2);
+ cursor: pointer;
+ transition: color 0.25s;
+
+ &:hover {
+ color: var(--vp-c-text-1);
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--vp-c-brand-1);
+ outline-offset: 2px;
+ }
+ }
+}
diff --git a/packages/docs/.vitepress/theme/styles/index.css b/packages/docs/.vitepress/theme/styles/index.css
index 823d6172..44593e4b 100644
--- a/packages/docs/.vitepress/theme/styles/index.css
+++ b/packages/docs/.vitepress/theme/styles/index.css
@@ -1,3 +1,5 @@
+@import './api-flavor.css';
+
:root {
--vp-c-brand-1: #5591d8;
--vp-c-brand-2: #336cb0;
diff --git a/packages/docs/advanced/lazy-queries.md b/packages/docs/advanced/lazy-queries.md
index 9e59df4e..76a1484a 100644
--- a/packages/docs/advanced/lazy-queries.md
+++ b/packages/docs/advanced/lazy-queries.md
@@ -2,6 +2,12 @@
[`useLazyQuery`](/api/composable/functions/useLazyQuery) is for queries where the variables are not known up front. Reach for it when a query should run in response to a user action (search submit, button click, modal open) rather than automatically on mount.
+:::: components-api
+::: warning Composition API only
+There is no ``.
+:::
+::::
+
## When to use lazy vs enabled
| Situation | Tool |
@@ -14,14 +20,8 @@
## Basic usage
-```vue twoslash
+```vue
```
-Without `clientId`, the default client handles the operation. The same option works on [`useMutation`](/data/mutations), [`useSubscription`](/data/subscriptions), [`useLazyQuery`](/advanced/lazy-queries), and [`useFragment`](/data/fragments).
+The same option works on [`useMutation`](/data/mutations), [`useSubscription`](/data/subscriptions), [`useLazyQuery`](/advanced/lazy-queries), and [`useFragment`](/data/fragments).
+::::
+
+:::: components-api
+Set the `clientId` prop:
+
+```vue
+
+
+
+
+
+ {{ data.pageViews }} views
+
+
+
+```
+
+[``](/api/components/ApolloMutation),
+[``](/api/components/ApolloSubscription) and
+[``](/api/components/ApolloFragment) take the same prop.
+[``](/api/components/ApolloSubscribeToMore) always uses whichever
+client its enclosing `` picked.
+
+Because the prop is a normal binding, a client can be chosen per element without any of the
+plumbing a composable would need:
+
+```vue-html
+
+```
+::::
## Resolving clients imperatively
@@ -85,24 +113,32 @@ await analytics.mutate({
`resolveClient()` with no argument returns the default client.
+:::: components-api
+This is for code, not templates. An element that already picks its client with `clientId`
+never needs to resolve one by hand, so reach for `useApolloClient` only where the call
+happens outside the template: a store action, a route guard, an event handler that talks to
+the client directly.
+::::
+
## Switching clients reactively
`clientId` is read each time the underlying observable is created. You can vary it based on a ref to switch clients at runtime, but the query is re-created whenever it changes, which means the previous result is lost and a new fetch starts.
-```ts twoslash
-import { TypedDocumentNode } from '@apollo/client'
-import { useQuery } from '@vue/apollo-composable'
-import { ref } from 'vue'
-
-declare const gql: (literals: TemplateStringsArray, ...placeholders: any[]) => TypedDocumentNode
-const QUERY: TypedDocumentNode = gql``
-// ---cut---
+:::: composition-api
+```ts
const env = ref<'default' | 'staging'>('default')
const { current } = useQuery(QUERY, () => ({
clientId: env.value,
}))
```
+::::
+
+:::: components-api
+```vue-html
+
+```
+::::
This is fine for occasional switches (an admin tool toggling between staging and prod). For per-feature splits, prefer setting `clientId` once per component or per query, not reactively.
@@ -126,6 +162,14 @@ const result = cleanup(() => {
See [Outside Components](/advanced/outside-components) for the full pattern.
+:::: components-api
+::: warning Composition API only
+`provideApolloClients` exists to give composables an injection context they would otherwise
+lack. Components run inside a template, so they always have one and resolve `clientId`
+against the `ApolloClients` map the app provided.
+:::
+::::
+
## When to reach for multiple clients
| Situation | Pattern |
diff --git a/packages/docs/advanced/outside-components.md b/packages/docs/advanced/outside-components.md
index 797730b6..33c96b54 100644
--- a/packages/docs/advanced/outside-components.md
+++ b/packages/docs/advanced/outside-components.md
@@ -2,7 +2,18 @@
Vue Apollo's composables rely on Vue's injection system to find the Apollo client. Inside a component's `
+
+
+
+
+ Loading...
+
+
+
+
{{ data.user.name }}
+
+
+
+
Recent activity
+
+
+ {{ entry.kind }}
+
+
+
+
+ Loading activity...
+
+
+
+
+```
+
+`#loading` covers only the gap before the first chunk. From there `#data` stays mounted
+for the rest of the stream.
+
+Listen for `@streamingResult` if you want each chunk imperatively, and use the default
+slot when you need `resultState` itself to tell `'streaming'` from `'complete'`.
+::::
The `resultState` lifecycle for a `@defer` query:
@@ -116,6 +171,7 @@ The `resultState` lifecycle for a `@defer` query:
Because deferred fields may be absent during `streaming`, their TypeScript types should reflect that. With GraphQL Codegen, this happens automatically through the `Incremental` / `Streaming` type helpers. With manual `TypedDocumentNode`, mark deferred fields as optional in your `TData` type.
+:::: composition-api
Inside your component:
```ts
@@ -133,6 +189,36 @@ if (current.value.resultState === 'complete') {
console.log(current.value.result.user.activity)
}
```
+::::
+
+:::: components-api
+The default slot hands you the same discriminated union, so `resultState` narrows `result`
+in the template exactly as it does in script. See
+[Two ways to read the result](/data/queries#two-ways-to-read-the-result) for how the two
+rendering modes differ:
+
+```vue
+
+
+
+
+
+
+
{{ result.user.name }}
+
+
+
+
+ {{ entry.kind }}
+
+
+
+
+
+```
+::::
## Combining with Suspense
@@ -154,6 +240,18 @@ const { current } = await useQuery(PROFILE_QUERY, {
Use this carefully: it negates the latency benefit of `@defer`. Most of the time, you want the default (render fast, fill in slow) and only opt into `awaitComplete` for screens where partial data would be misleading.
+:::: components-api
+`awaitComplete` reaches the component through `options`, and it still governs SSR prefetch
+there, so the server waits for the whole response before rendering:
+
+```vue-html
+
+```
+
+On the client it has nothing to act on, because `` cannot suspend. The slots
+render as chunks land either way. See [Suspense](/data/suspense) for why.
+::::
+
## Refetching a streaming query
Refetching a `@defer` query goes through the streaming lifecycle again. The previous `result` stays available while the new response streams in. `networkStatus` reports `refetch` initially, then `streaming`, then `ready`.
@@ -170,6 +268,7 @@ query Alphabet {
}
```
+:::: composition-api
The server can send letters one by one. `current.result.letters` grows as items arrive:
```ts
@@ -179,6 +278,11 @@ watch(() => current.value.result?.letters, (letters) => {
console.log(letters) // ['a'], ['a', 'b'], ['a', 'b', 'c'], ...
})
```
+::::
+
+:::: components-api
+The list grows in place, so a `v-for` inside `#data` simply re-renders as each chunk lands.
+::::
`@stream` shares the same `resultState` transitions as `@defer`.
diff --git a/packages/docs/api/components/ApolloFragment.md b/packages/docs/api/components/ApolloFragment.md
new file mode 100644
index 00000000..332372f3
--- /dev/null
+++ b/packages/docs/api/components/ApolloFragment.md
@@ -0,0 +1,58 @@
+# ApolloFragment
+
+
+
+## Props
+
+| Name | Type | Default | Description |
+| --- | --- | --- | --- |
+| `fragment` *(required)* | `DocumentNode \| TypedDocumentNode` | | A GraphQL fragment document. |
+| `from` *(required)* | `NonNullable>` | | Cache identifiable entity to read the fragment from.
Accepts a masked object from a parent query, a `{ __ref }` reference, or a cache ID string. |
+| `fragmentName` | `string \| undefined` | | Name of the fragment to use, when the document declares more than one. |
+| `variables` | `TVariables \| undefined` | | Variables used when reading the fragment. |
+| `optimistic` | `boolean \| undefined` | | Read from optimistic cache data. |
+| `clientId` | `string \| undefined` | | Name of the client to use, from the provided `ApolloClients` map. |
+| `options` | `useFragment.Options \| undefined` | | Escape hatch for any other `useFragment` option. |
+
+## Events
+
+| Name | Payload |
+| --- | --- |
+| `nextState` | `[state: RenameKey, "dataState", "resultState">, "data", "result">]` |
+
+## Slots
+
+### `#data`
+
+Opinionated mode. Providing this slot selects it, and `data` has every field.
+
+| Slot prop | Type |
+| --- | --- |
+| `data` | `TData` |
+
+### `#incomplete`
+
+Shown while fields are still missing from the cache. Opinionated mode only.
+
+| Slot prop | Type |
+| --- | --- |
+| `data` | `DeepPartial` |
+| `missing` | `MissingTree \| undefined` |
+
+### `#default`
+
+Raw mode. Receives the flattened fragment state.
+
+Rendered in both modes, so nested children still mount.
+
+Receives `RenameKey, "dataState", "resultState">, "data", "result">`.
+
+## Exposed
+
+A template ref receives the full [`useFragment.Result`](/api/composable/@vue/namespaces/useFragment/interfaces/Result) surface, with refs unwrapped.
+
+```vue
+
+
+
+```
diff --git a/packages/docs/api/components/ApolloMutation.md b/packages/docs/api/components/ApolloMutation.md
new file mode 100644
index 00000000..737f2d7c
--- /dev/null
+++ b/packages/docs/api/components/ApolloMutation.md
@@ -0,0 +1,42 @@
+# ApolloMutation
+
+
+
+## Props
+
+| Name | Type | Default | Description |
+| --- | --- | --- | --- |
+| `mutation` *(required)* | `TypedDocumentNode` | | The mutation document. |
+| `variables` | `TVariables \| undefined` | | Variables applied to every call, unless overridden in `mutate()`. |
+| `clientId` | `string \| undefined` | | Name of the client to use, from the provided `ApolloClients` map. |
+| `options` | `useMutation.Options \| undefined` | | Escape hatch for any other `useMutation` option. |
+
+## Events
+
+| Name | Payload |
+| --- | --- |
+| `error` | `[error: ErrorLike]` |
+| `done` | `[result: ApolloClient.MutateResult>]` |
+
+## Slots
+
+### `#default`
+
+| Slot prop | Type |
+| --- | --- |
+| `mutate` | `useMutation.MutateFunction` |
+| `loading` | `boolean` |
+| `error` | `ErrorLike \| undefined` |
+| `called` | `boolean` |
+| `result` | `TData \| null \| undefined` |
+| `reset` | `() => void` |
+
+## Exposed
+
+A template ref receives the full [`useMutation.Result`](/api/composable/@vue/namespaces/useMutation/interfaces/Result) surface, with refs unwrapped.
+
+```vue
+
+
+
+```
diff --git a/packages/docs/api/components/ApolloQuery.md b/packages/docs/api/components/ApolloQuery.md
new file mode 100644
index 00000000..a0848b89
--- /dev/null
+++ b/packages/docs/api/components/ApolloQuery.md
@@ -0,0 +1,88 @@
+# ApolloQuery
+
+
+
+## Props
+
+| Name | Type | Default | Description |
+| --- | --- | --- | --- |
+| `query` *(required)* | `TypedDocumentNode` | | The query document. |
+| `variables` | `TVariables \| undefined` | | Variables for the query. |
+| `disabled` | `boolean \| undefined` | `false` | Skip execution until a condition is met. |
+| `fetchPolicy` | `useQuery.Options["fetchPolicy"] \| undefined` | | How the query reads from and writes to the cache. |
+| `pollInterval` | `number \| undefined` | | Refetch every N milliseconds. |
+| `debounce` | `number \| undefined` | | Delay variable updates (ms). `loading` covers the window. |
+| `throttle` | `number \| undefined` | | Throttle variable updates (ms). `loading` covers the window. |
+| `keepPreviousResult` | `boolean \| undefined` | | Keep the previous result on screen while new variables are in flight.
Off by default, as in `useQuery`. Opt in for search and pagination, where clearing the list on every change flickers. |
+| `clientId` | `string \| undefined` | | Name of the client to use, from the provided `ApolloClients` map. |
+| `empty` | `((data: TData) => boolean) \| undefined` | | Decides whether a result counts as empty, selecting the `#empty` slot.
Without it `#empty` is never used, so `#data` handles the empty case itself. |
+| `options` | `useQuery.Options \| undefined` | | Escape hatch for any other `useQuery` option. |
+
+## Events
+
+| Name | Payload |
+| --- | --- |
+| `result` | `[data: TData]` |
+| `completeResult` | `[data: TData]` |
+| `partialResult` | `[data: TData]` |
+| `streamingResult` | `[data: TData]` |
+| `error` | `[error: ErrorLike]` |
+| `nextState` | `[state: useQuery.Current]` |
+
+## Slots
+
+### `#data`
+
+Opinionated mode. Providing this slot selects it, and `data` is never undefined.
+
+| Slot prop | Type |
+| --- | --- |
+| `data` | `TData` |
+| `error` | `ErrorLike \| undefined` |
+| `isPreviousResult` | `boolean` |
+| `loading` | `boolean` |
+| `pending` | `boolean` |
+| `refetch` | `(variables?: TVariables \| undefined) => Promise, "data", "result"> \| undefined>` |
+| `fetchMore` | `(options: ObservableQuery.FetchMoreOptions) => Promise, "data", "result">> \| undefined` |
+
+### `#loading`
+
+Shown while the first result is loading. Opinionated mode only.
+
+No slot props.
+
+### `#error`
+
+Shown when the query failed and there is nothing to display. Opinionated mode only.
+
+| Slot prop | Type |
+| --- | --- |
+| `error` | `ErrorLike` |
+| `refetch` | `(variables?: TVariables \| undefined) => Promise, "data", "result"> \| undefined>` |
+
+### `#empty`
+
+Shown when the `empty` prop returns `true`, and as the last branch when the query
+settles with nothing to show. Without the `empty` prop, `#data` owns the empty result.
+
+No slot props.
+
+### `#default`
+
+Raw mode. Receives the flattened query state and its methods.
+
+Rendered in both modes, so nested renderless children still mount.
+
+Receives `useQuery.Current> & Pick`.
+
+Every field of [`useQuery.Current`](/api/composable/@vue/namespaces/useQuery/interfaces/Current) is present, so `resultState` narrows `result` here exactly as it does in script.
+
+## Exposed
+
+A template ref receives the full [`useQuery.Result`](/api/composable/@vue/namespaces/useQuery/interfaces/Result) surface, with refs unwrapped.
+
+```vue
+
+
+
+```
diff --git a/packages/docs/api/components/ApolloSubscribeToMore.md b/packages/docs/api/components/ApolloSubscribeToMore.md
new file mode 100644
index 00000000..d51f38a4
--- /dev/null
+++ b/packages/docs/api/components/ApolloSubscribeToMore.md
@@ -0,0 +1,18 @@
+# ApolloSubscribeToMore
+
+
+
+## Props
+
+| Name | Type | Default | Description |
+| --- | --- | --- | --- |
+| `document` *(required)* | `TypedDocumentNode` | | The subscription document. |
+| `variables` | `TVariables \| undefined` | | Variables for the subscription. |
+| `updateQuery` | `SubscribeToMoreUpdateQueryFn \| undefined` | | Merges incoming subscription data into the parent query's result. |
+| `context` | `DefaultContext \| undefined` | | Context passed to the link chain for this subscription. |
+
+## Events
+
+| Name | Payload |
+| --- | --- |
+| `error` | `[error: ErrorLike]` |
diff --git a/packages/docs/api/components/ApolloSubscription.md b/packages/docs/api/components/ApolloSubscription.md
new file mode 100644
index 00000000..5a8cc961
--- /dev/null
+++ b/packages/docs/api/components/ApolloSubscription.md
@@ -0,0 +1,44 @@
+# ApolloSubscription
+
+
+
+## Props
+
+| Name | Type | Default | Description |
+| --- | --- | --- | --- |
+| `subscription` *(required)* | `TypedDocumentNode` | | The subscription document. |
+| `variables` | `TVariables \| undefined` | | Variables for the subscription. |
+| `disabled` | `boolean \| undefined` | `false` | Stop the subscription. |
+| `clientId` | `string \| undefined` | | Name of the client to use, from the provided `ApolloClients` map. |
+| `options` | `useSubscription.Options \| undefined` | | Escape hatch for any other `useSubscription` option. |
+
+## Events
+
+| Name | Payload |
+| --- | --- |
+| `result` | `[data: TData]` |
+| `error` | `[error: ErrorLike]` |
+| `complete` | `[]` |
+
+## Slots
+
+### `#default`
+
+| Slot prop | Type |
+| --- | --- |
+| `result` | `TData \| undefined` |
+| `loading` | `boolean` |
+| `error` | `ErrorLike \| undefined` |
+| `start` | `() => void` |
+| `stop` | `() => void` |
+| `restart` | `() => Promise` |
+
+## Exposed
+
+A template ref receives the full [`useSubscription.Result`](/api/composable/@vue/namespaces/useSubscription/interfaces/Result) surface, with refs unwrapped.
+
+```vue
+
+
+
+```
diff --git a/packages/docs/api/components/index.md b/packages/docs/api/components/index.md
new file mode 100644
index 00000000..452ff5e3
--- /dev/null
+++ b/packages/docs/api/components/index.md
@@ -0,0 +1,9 @@
+# Components API
+
+
+
+- [ApolloQuery](./ApolloQuery)
+- [ApolloMutation](./ApolloMutation)
+- [ApolloSubscription](./ApolloSubscription)
+- [ApolloSubscribeToMore](./ApolloSubscribeToMore)
+- [ApolloFragment](./ApolloFragment)
diff --git a/packages/docs/api/composable/@vue/namespaces/useLazyQuery/interfaces/Options.md b/packages/docs/api/composable/@vue/namespaces/useLazyQuery/interfaces/Options.md
index d795580f..4f9dd982 100644
--- a/packages/docs/api/composable/@vue/namespaces/useLazyQuery/interfaces/Options.md
+++ b/packages/docs/api/composable/@vue/namespaces/useLazyQuery/interfaces/Options.md
@@ -165,9 +165,9 @@ Debounce variable updates (ms).
Keep previous result while loading new data.
-The retained result is reported as a normal result — `resultState`, `result` and
-`partial` all describe it — with `isPreviousResult` set to `true` so it can be
-told apart from a fresh one.
+The retained result is reported as a normal result, so `resultState`, `result` and
+`partial` all describe it, with `isPreviousResult` set to `true` so it can be told
+apart from a fresh one.
#### Default Value
diff --git a/packages/docs/api/composable/@vue/namespaces/useLazyQuery/interfaces/Result.md b/packages/docs/api/composable/@vue/namespaces/useLazyQuery/interfaces/Result.md
index 388c613a..cbf4db28 100644
--- a/packages/docs/api/composable/@vue/namespaces/useLazyQuery/interfaces/Result.md
+++ b/packages/docs/api/composable/@vue/namespaces/useLazyQuery/interfaces/Result.md
@@ -430,6 +430,8 @@ Event triggered when query result data is received.
Fires when `resultState` is `'complete'`, `'partial'`, or `'streaming'`. Does not fire for `'empty'` state or errors.
+A result served from the cache during the `useQuery()` call is replayed on the next tick, so a handler registered right after the call still receives it.
+
***
### onStreamingResult
diff --git a/packages/docs/api/composable/@vue/namespaces/useQuery/interfaces/Options.md b/packages/docs/api/composable/@vue/namespaces/useQuery/interfaces/Options.md
index 47ec5710..f890b7a2 100644
--- a/packages/docs/api/composable/@vue/namespaces/useQuery/interfaces/Options.md
+++ b/packages/docs/api/composable/@vue/namespaces/useQuery/interfaces/Options.md
@@ -173,9 +173,9 @@ Reactive flag to enable/disable the query.
Keep previous result while loading new data.
-The retained result is reported as a normal result — `resultState`, `result` and
-`partial` all describe it — with `isPreviousResult` set to `true` so it can be
-told apart from a fresh one.
+The retained result is reported as a normal result, so `resultState`, `result` and
+`partial` all describe it, with `isPreviousResult` set to `true` so it can be told
+apart from a fresh one.
#### Default Value
diff --git a/packages/docs/api/composable/@vue/namespaces/useQuery/interfaces/Result.md b/packages/docs/api/composable/@vue/namespaces/useQuery/interfaces/Result.md
index 717900e7..92ae1d97 100644
--- a/packages/docs/api/composable/@vue/namespaces/useQuery/interfaces/Result.md
+++ b/packages/docs/api/composable/@vue/namespaces/useQuery/interfaces/Result.md
@@ -398,6 +398,8 @@ Event triggered when query result data is received.
Fires when `resultState` is `'complete'`, `'partial'`, or `'streaming'`. Does not fire for `'empty'` state or errors.
+A result served from the cache during the `useQuery()` call is replayed on the next tick, so a handler registered right after the call still receives it.
+
***
### onStreamingResult
diff --git a/packages/docs/api/index.md b/packages/docs/api/index.md
new file mode 100644
index 00000000..44ef92dd
--- /dev/null
+++ b/packages/docs/api/index.md
@@ -0,0 +1,97 @@
+# API Reference
+
+Vue Apollo ships two public packages. They are two ways to spell the same thing: the
+components are built on the composables and expose the same Apollo Client behaviour, so
+mixing them in one app, or in one component, is expected.
+
+| Package | Style | Reference |
+| --- | --- | --- |
+| [`@vue/apollo-composable`](./composable/) | `
+
+
+
+
+
+
+```
+::::
+
+## How the two references are generated
+
+- **Composable**: [TypeDoc](https://typedoc.org/) over the published declaration files.
+- **Components**: [`vue-component-meta`](https://github.com/vuejs/language-tools), which
+ attaches JSDoc to props, events and slot props.
+
+Both are regenerated by `pnpm api:generate` and are not edited by hand.
+
+## Types
+
+Every option and result type is namespaced under the function or component it belongs to:
+`useQuery.Options`, `useMutation.Result`, `useFragment.Current`. The namespaces are
+listed under [`@vue/apollo-composable`](./composable/), and the components' props link
+into them, since a component prop is almost always the matching composable option.
+
+For writing typed documents, see [TypeScript](/data/typescript).
diff --git a/packages/docs/caching/cache-updates.md b/packages/docs/caching/cache-updates.md
index f7ff6695..6011547d 100644
--- a/packages/docs/caching/cache-updates.md
+++ b/packages/docs/caching/cache-updates.md
@@ -2,16 +2,32 @@
When a mutation modifies data on the server, the local cache needs to reflect the change so the UI updates. Apollo Client handles many cases automatically, but not all. This page covers the patterns for keeping the cache in sync after a mutation.
+:::: components-api
+::: tip Reading this page with ``
+Cache updating is configured entirely through mutation options, and ``
+takes those through its `options` prop, which accepts the full
+[`useMutation.Options`](/api/composable/@vue/namespaces/useMutation/interfaces/Options)
+object:
+
+```vue-html
+
+```
+
+So wherever a snippet below reads `useMutation(CREATE_TODO, { ... })`, the `{ ... }` is what
+goes into `:options`. Callbacks like `update` are ordinary functions declared in
+`
+
+
+
+
+
+
+ {{ todo.text }}
+
+
+
+
+
+```
+
+```vue [TodoDetail.vue]
+
+
+
+
+
+
+
+
+ {{ data.todo.text }}
+
+
+
+```
+
+:::
+
+Both documents in `./queries` select `id` alongside the fields they render. Without the key
+field in the selection set the cache cannot file the two results under the same `Todo:5`
+entry, and the components stop tracking each other.
+::::
+
+After the toggle runs, the mutation result `{ id, completed }` updates `Todo:5` in the cache. Both queries see the change immediately. No `refetchQueries`, no manual subscription.
## When the cache is not enough
@@ -112,7 +167,7 @@ For each of those, you describe the cache change yourself or refetch the affecte
## Fetch policies
-`fetchPolicy` on `useQuery` controls how the cache is consulted on each call:
+`fetchPolicy` controls how the cache is consulted on each call:
| Policy | Behavior |
|--------|----------|
@@ -124,6 +179,14 @@ For each of those, you describe the cache change yourself or refetch the affecte
`cache-and-network` is a useful default for screens where freshness matters but you do not want to block on the network: users see the cached result immediately, then the screen updates when the request returns.
+:::: components-api
+`` takes the policy as a prop:
+
+```vue-html
+
+```
+::::
+
## Configuration
Apollo Client's cache is configurable. You can tell it how to identify entities, how to merge nested fields, and how to handle pagination. The configuration lives where you create your `InMemoryCache`:
diff --git a/packages/docs/data/data-masking.md b/packages/docs/data/data-masking.md
index dfa4dadc..2c0cdca9 100644
--- a/packages/docs/data/data-masking.md
+++ b/packages/docs/data/data-masking.md
@@ -80,6 +80,7 @@ With masking on, fields defined only in a fragment are hidden from queries that
## Reading masked data
+:::: composition-api
Use [`useFragment`](/api/composable/functions/useFragment) inside the component that owns the fragment:
```vue twoslash
@@ -114,6 +115,49 @@ const { current } = useFragment({
```
`current.result` only contains the fields defined in the fragment. Parent queries do not leak into it, and sibling fragments do not leak either.
+::::
+
+:::: components-api
+Use [``](/api/components/ApolloFragment) inside the component that owns the
+fragment. The masked object the parent passed down goes straight into `from`:
+
+```vue twoslash
+
+
+
+
+
+
{{ data.title }}
+
{{ data.publishedAt }}
+
+
+
+```
+
+`data` only contains the fields defined in the fragment. Parent queries do not leak into it,
+and sibling fragments do not leak either.
+
+With masking enabled, a component cannot read the fragment's fields from the `post` prop
+it receives. `` unmasks them in the template.
+::::
## Fixing the parent component
@@ -178,7 +222,12 @@ This helps you find every implicit dependency before removing the `@unmask` dire
| `subscribeToMore` `updateQuery` callback | No |
| Cache APIs (`readQuery`, `readFragment`) | No |
-Cache APIs and mutation update callbacks deal with the underlying cache directly, so masking does not apply to them.
+:::: components-api
+Every row above applies to the components too. A masked field is missing from
+[``](/api/components/ApolloQuery)'s `#data` slot and from its `@result`
+payload, and still present inside an `update` callback passed through
+[``](/api/components/ApolloMutation)'s `options` prop.
+::::
## Incremental adoption
@@ -216,7 +265,7 @@ If you use TypeScript with GraphQL Codegen:
### 4. Refactor components
1. Watch the console for "accessing masked field" warnings.
-2. Update components to use `useFragment` for the data they own.
+2. Read the data a component owns through [`useFragment`](/api/composable/functions/useFragment) or [``](/api/components/ApolloFragment).
3. Add any required fields to parent queries explicitly.
4. Remove `@unmask` directives when no warnings remain.
diff --git a/packages/docs/data/error-handling.md b/packages/docs/data/error-handling.md
index 5c02810d..db6b6fee 100644
--- a/packages/docs/data/error-handling.md
+++ b/packages/docs/data/error-handling.md
@@ -57,6 +57,7 @@ Network errors are represented by several classes:
### Queries
+:::: composition-api
Read errors through `current.error`:
```vue twoslash
@@ -85,9 +86,50 @@ const { current } = useQuery(gql`
```
+::::
+
+:::: components-api
+The `#error` slot covers the case where the query failed and there is nothing to show. It
+receives `refetch`, so the retry lives in the template:
+
+```vue twoslash
+
+
+
+
+
+ Loading...
+
+
+
+ Error: {{ error.message }}
+
+
+
+
+ {{ data.users.length }} users
+
+
+
+```
+
+Slot resolution is data-first, so a refetch that fails over data already on screen keeps
+rendering `#data` rather than replacing a working page with an error. Those failures reach
+the `@error` event, where a toast is usually the right treatment. See
+[Two ways to read the result](/data/queries#two-ways-to-read-the-result).
+::::
### Mutations
+:::: composition-api
Read errors through the `error` ref:
```vue twoslash
@@ -110,9 +152,42 @@ const { mutate, error, loading } = useMutation(gql`
```
+::::
+
+:::: components-api
+`error` is a slot prop on the default slot:
+
+```vue
+
+
+
+ console.error('Create failed:', failure)"
+ >
+
+
+ {{ error.message }}
+
+
+
+```
+
+`` bridges `@error` to `onError` only while the parent is actually
+listening, so binding the event is what makes `mutate()` resolve here. Leave `@error`
+unbound and the same call rejects instead, exactly as it would under `useMutation`'s
+`throws: 'auto'`. See [Error throwing behavior](/data/mutations#error-throwing-behavior).
+::::
### Subscriptions
+:::: composition-api
Read errors through the `error` ref, or register an `onError` callback:
```vue twoslash
@@ -139,9 +214,39 @@ onError((err) => {
```
+::::
+
+:::: components-api
+Read errors from the `error` slot prop, next to the `restart` that reconnects:
+
+```vue twoslash
+
+
+
+
+
+ Connection error: {{ error.message }}
+
+
+
+
+```
+
+The same failure is also emitted as `@error`, which is what
+[Error event hooks](#error-event-hooks) below covers.
+::::
## Error event hooks
+:::: composition-api
Every composable exposes an `onError` event hook for imperative handling:
```ts twoslash
@@ -163,11 +268,48 @@ onError((error) => {
// errorTracker.capture(error)
})
```
+::::
+
+:::: components-api
+``, ``, `` and ``
+emit `@error`. `` has none.
+
+```vue
+
+
+
+
+
+ {{ data.users.length }}
+
+
+
+```
+
+Unlike `#error`, the event fires for every failure, including one that lands on top of a
+result the reader is still looking at. That makes it the right place for logging and
+toasts, and `#error` the right place for the empty-page treatment.
+::::
## Mutation throwing behavior
By default, `mutate()` throws when no `onError` handler is registered. The `throws` option controls this:
+:::: composition-api
```ts twoslash
import { TypedDocumentNode } from '@apollo/client'
import { useMutation } from '@vue/apollo-composable'
@@ -208,6 +350,7 @@ async function handleSubmit() {
}
}
```
+::::
| Value | Behavior |
|-------|----------|
@@ -215,10 +358,25 @@ async function handleSubmit() {
| `'always'` | Always throws |
| `'never'` | Never throws |
+:::: components-api
+`` has the same default. Binding `@error` is what registers a handler under
+`'auto'`, so `mutate()` resolves and the failure arrives on the event; with nothing bound it
+rejects. `throws` has no dedicated prop, so set it through `options`, which takes the full
+[`useMutation.Options`](/api/composable/@vue/namespaces/useMutation/interfaces/Options)
+object:
+
+```vue-html
+
+```
+
+See [Error throwing behavior](/data/mutations#error-throwing-behavior) for the full table.
+::::
+
## Error policies
By default, Apollo Client discards partial data when a GraphQL error occurs and populates `error`. Change this with `errorPolicy`:
+:::: composition-api
```ts twoslash
import { TypedDocumentNode } from '@apollo/client'
import { useQuery } from '@vue/apollo-composable'
@@ -230,6 +388,16 @@ const { current } = useQuery(GET_USERS, {
errorPolicy: 'all',
})
```
+::::
+
+:::: components-api
+`errorPolicy` has no dedicated prop; set it through `options`, which takes the full
+[`useQuery.Options`](/api/composable/@vue/namespaces/useQuery/interfaces/Options) object:
+
+```vue-html
+
+```
+::::
| Policy | Behavior |
|--------|----------|
@@ -241,6 +409,7 @@ const { current } = useQuery(GET_USERS, {
With `errorPolicy: 'all'`, partial data is available alongside the error:
+:::: composition-api
```vue twoslash
+
+
+
+
+ Some data couldn't be loaded: {{ error.message }}
+
+
+ Good field: {{ result.goodField }}
+
+
+
+```
+
+The two modes mix, so a `#data` slot can still handle the everything-worked case while the
+default slot renders the warning banner above it.
+::::
## Identifying error types
@@ -359,8 +563,8 @@ const errorLink = new ErrorLink(({ error, operation, forward }) => {
})
```
-::: warning
-If the retried operation also fails, those errors do not reach `ErrorLink` again. An `ErrorLink` can only retry a particular operation once.
+::: warning `ErrorLink` retries an operation only once
+If the retried operation also fails, those errors do not reach `ErrorLink` again.
:::
### Retry on network errors
@@ -390,6 +594,7 @@ const link = from([retryLink, new HttpLink({ uri: '/graphql' })])
`reset()` clears mutation errors so the form can be tried again:
+:::: composition-api
```vue twoslash
+
+
+
+
+
+ {{ error.message }}
+
+
+
+
+```
+
+See [Resetting state](/data/mutations#resetting-state) for what else it clears.
+::::
## Next steps
diff --git a/packages/docs/data/fragments.md b/packages/docs/data/fragments.md
index 5f3571ae..df933db1 100644
--- a/packages/docs/data/fragments.md
+++ b/packages/docs/data/fragments.md
@@ -30,14 +30,24 @@ const GET_USER = gql`
`
```
-## Reading fragment data with `useFragment`
+## Reading fragment data
+:::: composition-api
[`useFragment`](/api/composable/functions/useFragment) creates a reactive binding to fragment data in the Apollo cache. It watches for changes and updates automatically when the cache changes.
+::::
+
+:::: components-api
+[``](/api/components/ApolloFragment) creates a reactive binding to fragment
+data in the Apollo cache. It watches for changes and updates automatically when the cache
+changes.
+::::
::: warning Cache identification required
-`useFragment` only works with entities the cache can identify. Each entity needs a unique cache ID, normally `__typename` plus the entity's key field (usually `id`).
+This only works with entities the cache can identify. Each entity needs a unique cache ID,
+normally `__typename` plus the entity's key field (usually `id`).
:::
+:::: composition-api
```vue twoslash
+
+
+
+
+ {{ data.name }} ({{ data.email }})
+
+
+
+```
+
+The `from` prop accepts an object with `__typename` and the key field, a reference object
+like `{ __ref: 'User:1' }`, or a string cache ID such as `'User:1'`.
+
+Like ``, the component has two modes. Providing `#data` selects the
+opinionated one, where `data` has every field the fragment asks for. `#incomplete` covers
+the case where the cache is missing some of them, and receives both the partial data and a
+`missing` tree describing what is absent:
+
+```vue-html
+
+
+
Partial record for {{ data.id }}
+
{{ missing }}
+
+
+ {{ data.name }}
+
+
+```
+
+Using only the default slot gives you raw mode, with the flattened
+[`useFragment.Current`](/api/composable/@vue/namespaces/useFragment/interfaces/Current)
+state to narrow yourself.
+::::
### Working with arrays
+:::: composition-api
Pass an array of entities to read several at once:
```ts twoslash
@@ -94,9 +171,47 @@ const { current } = useFragment({
```
`current.result` is an array of items where each index lines up with `from`. `resultState` is `'complete'` only when every item is complete.
+::::
+
+:::: components-api
+`` reads a **single** entity. For a list, put it in a `v-for`:
+
+```vue twoslash
+
+
+
+
+
+
+
+ {{ data.name }}
+
+
+
+
+
+```
+
+`useFragment`'s array form reports `resultState` as `'complete'` only when *every* entity
+is complete, so one entity missing a field downgrades the whole list. One component per
+row gives each entity its own complete/incomplete branch, and `#data` stays typed as a
+single entity instead of an array.
+::::
### Event hook
+:::: composition-api
React to fragment data changes imperatively:
```ts twoslash
@@ -116,6 +231,19 @@ onNextState((state) => {
console.log('Fragment data changed:', state)
})
```
+::::
+
+:::: components-api
+`onNextState` is emitted as `@nextState`:
+
+```vue-html
+ console.log('Fragment data changed:', state)"
+/>
+```
+::::
## Colocating fragments
@@ -129,6 +257,7 @@ Colocate fragment definitions with the components that read them. Each component
Since `export const` is not allowed in `
+
+
+```
+
+```vue [UserProfile.vue]
+
+
+
+
+
+
+
+ {{ data.user.bio }}
+
+
+
+```
+
+:::
+::::
::: tip Fragment naming
Prefix fragment names with the component name (`UserAvatarFields`) so they remain identifiable when many fragments are composed together.
@@ -217,10 +404,20 @@ const GET_USER = gql`
## Options and result reference
+:::: composition-api
For every available option and method, see:
- [`useFragment.Options`](/api/composable/@vue/namespaces/useFragment/interfaces/Options)
- [`useFragment.Result`](/api/composable/@vue/namespaces/useFragment/interfaces/Result)
+::::
+
+:::: components-api
+For every prop, event and slot prop, see:
+
+- [``](/api/components/ApolloFragment)
+- [`useFragment.Options`](/api/composable/@vue/namespaces/useFragment/interfaces/Options), for the `options` prop
+- [`useFragment.Result`](/api/composable/@vue/namespaces/useFragment/interfaces/Result), for what a template ref exposes
+::::
## Next steps
diff --git a/packages/docs/data/mutations.md b/packages/docs/data/mutations.md
index acdbf92a..77dc054d 100644
--- a/packages/docs/data/mutations.md
+++ b/packages/docs/data/mutations.md
@@ -1,9 +1,12 @@
# Mutations
-This page covers updating data with the [`useMutation`](/api/composable/functions/useMutation) composable.
+A GraphQL mutation is a write request. Like a [query](/data/queries) it names the fields it wants back, so the server returns the updated object in the same round trip that changed it.
+
+That return value is what makes mutations more than a POST. Apollo Client writes it into the same normalized cache your queries read from, so a mutation that returns the entity it modified updates every query already showing it, with no refetch and no manual invalidation.
## Executing a mutation
+:::: composition-api
Unlike [`useQuery`](/data/queries), `useMutation` does not execute automatically. It returns a `mutate` function that you call when you want the mutation to run:
```vue twoslash
@@ -52,13 +55,67 @@ async function handleSubmit() {
- `called` is `true` once `mutate` has been called at least once.
- `result` holds the most recent mutation result data.
- `reset()` resets `result`, `error`, `loading`, and `called` to their initial state.
+::::
-::: tip Why flat refs and not `current`?
-`useMutation` does not expose a `current` discriminated union the way `useQuery` does. A mutation is request-response: it does not have streaming or partial states. Using individual refs keeps the API close to how you naturally consume a mutation. See [TypeScript](/data/typescript#composable-return-value-shapes) for the comparison.
-:::
+:::: components-api
+`` is renderless: it has one slot, and nothing happens until you call
+`mutate` from it.
+
+```vue twoslash
+
+
+
+
+
+
+ Error: {{ error.message }}
+
+
+
+```
+
+Nothing is listening for `@error` here, so `mutate()` rejects on failure: the chain needs a
+`.catch`, and the input is cleared only when the mutation actually succeeded. Bind `@error`
+instead and `mutate()` resolves, with the failure arriving on the event. See
+[Error throwing behavior](#error-throwing-behavior).
+
+The slot gives you the same surface `useMutation` returns, with the refs unwrapped:
+
+- `mutate(options?)` triggers the mutation and returns a promise resolving to the result.
+- `loading` is `true` while the mutation is in flight.
+- `error` contains any error from the mutation.
+- `called` is `true` once `mutate` has been called at least once.
+- `result` holds the most recent mutation result data.
+- `reset()` clears `result`, `error` and `called`.
+::::
## Variables
+:::: composition-api
The most common pattern is to pass variables when calling `mutate`:
```ts twoslash
@@ -94,9 +151,27 @@ mutate()
```
When variables come from both composable options and the `mutate` call, the call-time variables win, merged on top of the composable variables.
+::::
+
+:::: components-api
+Pass variables when calling `mutate` from the slot:
+
+```vue-html
+
```
+::::
+
+:::: components-api
+`start`, `stop` and `restart` are all slot props:
+
+```vue twoslash
+
+
+
+
+
+ Pause
+
+
+ Resume
+
+
+ Reconnect
+
+
+
+```
+
+For pausing declaratively, prefer the `disabled` prop below, which survives re-renders.
+::::
### Conditionally enabling
+:::: composition-api
Use `enabled` to gate the subscription on a condition:
```ts twoslash
@@ -319,9 +421,26 @@ const { result } = useSubscription(NOTIFICATIONS, {
```
While `enabled` is `false`, no connection exists. When it flips to `true`, the subscription starts.
+::::
+
+:::: components-api
+Use the `disabled` prop:
+
+```vue-html
+
+```
+
+While `disabled` is `true`, no connection exists. When it flips to `false`, the subscription
+starts.
+
+As with [``](/data/queries#disabling-queries), the prop is `disabled` rather
+than the composable's `enabled`, so that an absent prop means "on". `options: { enabled }`
+still works.
+::::
## Event hooks
+:::: composition-api
```ts twoslash
import { TypedDocumentNode } from '@apollo/client'
import { useSubscription } from '@vue/apollo-composable'
@@ -347,9 +466,35 @@ onComplete(() => {
```
`onComplete` fires when the server closes the subscription cleanly (for example, after a finite stream like a countdown).
+::::
+
+:::: components-api
+The three hooks are emitted as `@result`, `@error` and `@complete`:
+
+```vue
+
+
+
+ console.log('New message:', result.newMessage)"
+ @error="error => console.error('Subscription error:', error)"
+ @complete="() => console.log('Subscription completed')"
+ />
+
+```
+
+`@complete` fires when the server closes the subscription cleanly (for example, after a
+finite stream like a countdown).
+::::
## Subscribing to query updates
+:::: composition-api
`subscribeToMore` lets you fetch initial data with a query and stream updates into it via a subscription. The merged result behaves like a single, continuously-updated query.
```vue twoslash
@@ -414,15 +559,111 @@ watch(
```
The first argument to `updateQuery` (`_prev`) is deprecated in Apollo Client v4. Read from `options.previousData` with the `options.complete` guard for type-safe access.
+::::
+
+:::: components-api
+[``](/api/components/ApolloSubscribeToMore) does this declaratively.
+Drop it inside an `` and it subscribes to the query it finds, with no `watch`
+and no waiting for the query to load first:
+
+```vue
+
+
+
+
+
+
+
+ Loading...
+
+
+ Error: {{ error.message }}
+
+
+
+
+ {{ msg.text }}
+
+
+
+
+
+```
+
+The component renders nothing. It subscribes on mount, unsubscribes on unmount, and
+resubscribes when `document`, `variables` or `context` change. Changing `updateQuery`
+alone never resubscribes.
+
+The `context` prop is passed to the link chain for this subscription alone, which is where
+per-subscription headers or link options belong:
+
+```vue-html
+
+```
+
+::: warning It must be inside an ``
+`` injects the surrounding query, and throws on mount if there is
+none. It can sit anywhere in the default slot, including alongside `#data`.
+
+The parent query's types are not visible to it, so `updateQuery` is checked against the
+subscription's types only. Annotate the callback yourself, as above, to get the parent
+query's result type back.
+:::
+
+Errors from the subscription surface on its own `@error` event rather than on the query.
See [`SubscribeToMoreOptions`](/api/composable/@vue/namespaces/useQuery/interfaces/SubscribeToMoreOptions) for all available options.
+::::
+
+:::: composition-api
+See [`SubscribeToMoreOptions`](/api/composable/@vue/namespaces/useQuery/interfaces/SubscribeToMoreOptions) for all available options.
+::::
## Options and result reference
+:::: composition-api
For every available option and method, see:
- [`useSubscription.Options`](/api/composable/@vue/namespaces/useSubscription/interfaces/Options)
- [`useSubscription.Result`](/api/composable/@vue/namespaces/useSubscription/interfaces/Result)
+::::
+
+:::: components-api
+For every prop, event and slot prop, see:
+
+- [``](/api/components/ApolloSubscription)
+- [``](/api/components/ApolloSubscribeToMore)
+- [`useSubscription.Options`](/api/composable/@vue/namespaces/useSubscription/interfaces/Options), for the `options` prop
+::::
## Next steps
diff --git a/packages/docs/data/suspense.md b/packages/docs/data/suspense.md
index 9b925972..33f1cc02 100644
--- a/packages/docs/data/suspense.md
+++ b/packages/docs/data/suspense.md
@@ -2,6 +2,16 @@
Vue's built-in `` component lets you display a fallback while async dependencies resolve. Vue Apollo plugs into Suspense by exposing `useQuery` as `PromiseLike`, so you can `await` it in your component's setup.
+:::: components-api
+::: warning Composition API only
+`` cannot suspend. Suspense requires the `await` to happen in the suspending
+component's own `setup`.
+
+Use `await useQuery(...)` in `
diff --git a/packages/docs/data/typescript.md b/packages/docs/data/typescript.md
index d0e15e3b..7160bd56 100644
--- a/packages/docs/data/typescript.md
+++ b/packages/docs/data/typescript.md
@@ -128,6 +128,7 @@ yarn add -D @parcel/watcher
Import the `graphql` function from the generated output and define your queries inline:
+:::: composition-api
```vue
+
+
+
+
+ Loading...
+
+
+ Error: {{ error.message }}
+
+
+
{{ data.project.name }}
+
+
+
+
+```
+
+The components are generic SFCs, so `TData` and `TVariables` flow from the `query` prop into
+the slot props and the event payloads. Passing the wrong `variables` shape is a type error,
+and `data.project` is checked against the document.
+
+::: warning Import the components, do not register them globally
+Global registration erases the generics, and every slot prop falls back to `any`. The same
+applies to a plain `DocumentNode` with no type parameters: annotate it, or generate it, to
+keep inference.
+:::
+::::
The `graphql()` function:
@@ -204,6 +258,7 @@ Place this file (for example `apollo-client.d.ts`) somewhere your `tsconfig.json
Use `FragmentType` from `@apollo/client` to type component props that receive fragment data:
+:::: composition-api
```vue
+
+
+
+
+
+
+
+
+```
+
+This pattern:
+
+- Uses `FragmentType` so the parent must pass a correctly-typed fragment reference.
+- Reads the fragment from the cache with [``](/api/components/ApolloFragment),
+ whose `from` prop takes the masked object straight from the prop.
+- Renders `#data` only when every field is present, so `data` is the complete fragment type
+ rather than a partial one. `#incomplete` covers the rest.
+- Works with [Data Masking](/data/data-masking) for isolated component data.
+::::
-## Composable return-value shapes
+## Result shapes
-Vue Apollo's composables expose different result shapes depending on what makes sense for each operation. The table below shows what each composable returns:
+The table below shows what each composable returns:
| Composable | `current` ref (discriminated union) | Individual refs |
|------------|-------------|------|
@@ -274,7 +379,17 @@ if (current.value.resultState === 'complete') {
The individual refs remain available for code that only reads `loading` or `error` without accessing `result`.
-For `useMutation` and `useSubscription` the discriminated union is not provided because their results do not have multiple data states. A mutation is request-response, and a subscription delivers one result at a time.
+For `useMutation` and `useSubscription` the discriminated union is not provided because their results do not have multiple data states.
+
+:::: components-api
+::: tip Where the components fit
+The components hand these same shapes to their slots. `#data` types its `data` as the
+`'complete'` branch, but it renders for `partial` and `streaming` too, so the default slot
+is the one that narrows honestly: it receives the whole `current` union and the rules in
+the next section apply verbatim inside `v-slot`. A template ref gets the "individual refs"
+column with the refs unwrapped.
+:::
+::::
## Type narrowing with `resultState`
@@ -287,6 +402,7 @@ The `resultState` discriminator narrows `result` precisely. The states for `useQ
| `'streaming'` | Data streaming via `@defer` or `@stream` | `TData` |
| `'empty'` | No data yet | `undefined` |
+:::: composition-api
```ts
const { current } = useQuery(GET_USERS, { returnPartialData: true })
@@ -300,6 +416,45 @@ else if (current.value.resultState === 'streaming') {
// TData (still arriving)
}
```
+::::
+
+:::: components-api
+The default slot receives the same discriminator, so the narrowing happens in the template:
+
+```vue twoslash
+
+
+
+
+
+
+ {{ user.name }}
+
+
+
+ {{ result.users?.length ?? 0 }} users cached so far
+
+
+ Still loading the rest...
+
+
+
+```
+
+Use the default slot whenever the distinction matters. `#data` renders for `'partial'` and
+`'streaming'` too, and it types `data` as the complete result in all three cases, so a
+partial result reaches it with fields the type claims are there.
+::::
For `useFragment` the states are `'complete'` and `'partial'`, and follow the same narrowing pattern.
@@ -307,20 +462,36 @@ For `useFragment` the states are `'complete'` and `'partial'`, and follow the sa
TypeScript validates required variables and their types:
+:::: composition-api
```ts
// TypeScript Error: Property 'variables' is missing
-const { current } = useQuery(GET_USER_QUERY)
+const { current } = useQuery(GET_USER)
// TypeScript Error: Property 'id' is missing
-const { current } = useQuery(GET_USER_QUERY, { variables: {} })
+const { current } = useQuery(GET_USER, { variables: {} })
// OK
-const { current } = useQuery(GET_USER_QUERY, {
+const { current } = useQuery(GET_USER, {
variables: { id: '1' },
})
```
When variables are entirely optional (the query has no required variables), the `variables` option itself is optional.
+::::
+
+:::: components-api
+```vue-html
+
+
+
+
+
+```
+
+Because `variables` is a prop rather than a required argument, a missing `variables`
+altogether is not caught. Everything inside the object is checked, so this only affects the
+all-or-nothing case.
+::::
## Manual TypedDocumentNode
@@ -343,8 +514,8 @@ const GET_USERS: TypedDocumentNode = gql`
`
```
-::: tip
-Always provide the variables type. For queries with no variables, use `Record` so accidental variables produce a type error.
+::: tip Always provide the variables type
+For queries with no variables, use `Record` so accidental variables produce a type error.
:::
## Next steps
diff --git a/packages/docs/guide/index.md b/packages/docs/guide/index.md
index 9dfdb5f4..660224e3 100644
--- a/packages/docs/guide/index.md
+++ b/packages/docs/guide/index.md
@@ -6,9 +6,22 @@
Ready to try it out? Skip to the Installation.
+## Two APIs, one library
+
+Vue Apollo ships two packages, and the selector at the top of the sidebar switches this
+guide between them:
+
+| Package | What you write | Reach for it when |
+|---|---|---|
+| [`@vue/apollo-composable`](/api/composable/) | `useQuery` and friends in `
+
+
+
+
+ Loading...
+
+
+
+
+ {{ user.name }}
+
+
+
+
+
+```
+::::
## Compatibility
@@ -52,6 +103,8 @@ const { current } = useQuery(gql`
| Vue | 3.5+ |
| Apollo Client | 4.1+ |
+Vue 2 is no longer supported. See [What's changed in v5](/migration/whats-changed).
+
::: warning Apollo Client 4.1 required
Vue Apollo requires `@apollo/client` version 4.1.0 or higher. The features Vue Apollo depends on (improved TypeScript inference, the `DataState` discriminated union, incremental delivery handlers) are only available from 4.1 onward.
:::
diff --git a/packages/docs/guide/installation.md b/packages/docs/guide/installation.md
index 3b607f49..e54e1171 100644
--- a/packages/docs/guide/installation.md
+++ b/packages/docs/guide/installation.md
@@ -4,6 +4,7 @@
Install Apollo Client and Vue Apollo:
+:::: composition-api
::: code-group
```shell [npm]
@@ -19,11 +20,40 @@ pnpm add @apollo/client @vue/apollo-composable@next graphql
```
:::
+::::
+
+:::: components-api
+`@vue/apollo-components` lists `@vue/apollo-composable` as a peer dependency, so install
+both.
+
+::: code-group
+
+```shell [npm]
+npm install @apollo/client @vue/apollo-composable@next @vue/apollo-components@next graphql
+```
+
+```shell [yarn]
+yarn add @apollo/client @vue/apollo-composable@next @vue/apollo-components@next graphql
+```
+
+```shell [pnpm]
+pnpm add @apollo/client @vue/apollo-composable@next @vue/apollo-components@next graphql
+```
+
+:::
+::::
::: warning Pre-release version
Vue Apollo v5 is currently in pre-release. The `@next` tag installs the latest pre-release build.
:::
+::: tip Choosing an API
+The selector at the top of the sidebar switches every example in the guide between the
+**Composition API** (`useQuery` and friends) and the **Components API** (``
+and friends). They are the same library, so you can pick per component and change your
+mind later. See the [API overview](/api/) for how to choose.
+:::
+
## Step 2: Create an Apollo Client
Create a file to configure your Apollo Client instance:
@@ -68,12 +98,20 @@ app.provide(DefaultApolloClient, apolloClient)
app.mount('#app')
```
+:::: composition-api
That's it. You can now use [`useQuery`](/api/composable/functions/useQuery.md), [`useMutation`](/api/composable/functions/useMutation.md), and the other composables in any component.
+::::
+
+:::: components-api
+That's it. You can now use [``](/api/components/ApolloQuery),
+[``](/api/components/ApolloMutation) and the rest in any template.
+::::
## Step 4: Your first query
Verify the setup with a simple query:
+:::: composition-api
```vue twoslash
+
+
+
+
+ Loading...
+
+
+ Error: {{ error.message }}
+
+
+ {{ data.hello }}
+
+
+
+```
+::::
## IDE integration
diff --git a/packages/docs/guide/why-apollo.md b/packages/docs/guide/why-apollo.md
index 38ed64c0..5ab9efcb 100644
--- a/packages/docs/guide/why-apollo.md
+++ b/packages/docs/guide/why-apollo.md
@@ -6,6 +6,7 @@ Apollo Client is a comprehensive state-management library for JavaScript. With G
You write a query, and Apollo Client handles fetching, caching, and updating the UI:
+:::: composition-api
```vue twoslash
```
+::::
+
+:::: components-api
+```vue twoslash
+
+
+
+
+
+ {{ data.todos.length }} todos
+
+
+
+```
+::::
You do not need to track loading states by hand, juggle error branches, or update the cache after every mutation. Apollo Client handles all of it.
@@ -44,7 +76,10 @@ Because the cache is normalized by `__typename` + `id`, any query that reads an
## Vue-native reactivity
-Vue Apollo plugs into Vue's reactivity system. Query variables can be refs, reactive objects, getters, or even per-key reactive maps:
+Vue Apollo plugs into Vue's reactivity system.
+
+:::: composition-api
+Query variables can be refs, reactive objects, getters, or even per-key reactive maps:
```vue twoslash
+
+
+
+
+ {{ data.user.name }}
+
+
+
+```
+
+When `userId` changes, the query re-executes with the new value. No refs inside the object, no getters, no watcher boilerplate.
+::::
## TypeScript support
@@ -77,7 +150,6 @@ With [GraphQL Codegen](/data/typescript), every query, mutation, and result is f
```ts twoslash
import type { TypedDocumentNode } from '@apollo/client'
-import { useQuery } from '@vue/apollo-composable'
declare const graphql: (q: string) => TypedDocumentNode<{ user: { id: string, name: string, email: string } }, { id: string }>
@@ -91,11 +163,15 @@ const UserQuery = graphql(`
}
}
`)
-
-const { current } = useQuery(UserQuery, { variables: { id: '1' } })
```
+:::: composition-api
`current.result` is typed precisely, including narrowing by `current.resultState` so partial and streaming states are handled safely.
+::::
+
+:::: components-api
+Passing that document to `` types its `variables` prop, its slot props and its event payloads, and `#data` hands you the fully-resolved shape with no narrowing to write.
+::::
## When to use Apollo Client
@@ -106,4 +182,6 @@ Apollo Client is a good fit when:
- You want real-time updates through subscriptions, `@defer`, or `@stream`.
- You value TypeScript correctness throughout the stack.
-For simple REST APIs or apps that do little caching, lighter alternatives exist. For GraphQL apps with non-trivial caching needs, Apollo Client is the most complete option.
+If your backend speaks REST rather than GraphQL, [rstore](https://rstore.dev/) covers much of the same ground for Vue and Nuxt.
+
+For apps that do little caching, something lighter still may be enough. For GraphQL apps with non-trivial caching needs, Apollo Client is the most complete option.
diff --git a/packages/docs/index.md b/packages/docs/index.md
index b613687b..6bf67042 100644
--- a/packages/docs/index.md
+++ b/packages/docs/index.md
@@ -13,13 +13,13 @@ hero:
link: /guide/
- theme: alt
text: API Reference
- link: /api/composable/
+ link: /api/
features:
- title: Automatic updates
details: Don't think about updating the UI or refetching the queries!
icon: ✨
-- title: Supports all Vue APIs
- details: Option API, Composition API or Components
+- title: Composables or components
+ details: useQuery in script, or ApolloQuery in the template. Same client, same cache.
icon: 🧩
- title: SSR-ready
details: Run your queries on the server before rendering the page HTML
diff --git a/packages/docs/local-state/overview.md b/packages/docs/local-state/overview.md
index 123c0143..bbb1d509 100644
--- a/packages/docs/local-state/overview.md
+++ b/packages/docs/local-state/overview.md
@@ -19,6 +19,7 @@ export const theme = ref<'light' | 'dark'>('light')
```ts
import { defineStore } from 'pinia'
+import { ref } from 'vue'
export const useAuthStore = defineStore('auth', () => {
const token = ref(null)
@@ -63,14 +64,14 @@ Apollo Client provides [`makeVar`](https://www.apollographql.com/docs/react/loca
Vue Apollo intentionally does not ship a `useReactiveVar` composable, for two reasons:
-1. Vue already has a complete reactivity system (`ref`, `computed`, `watch`). It does not need a parallel one.
+1. Vue already has a complete reactivity system (`ref`, `computed`, `watch`).
2. For nearly all use cases, a Vue ref or Pinia store is a better fit than `makeVar`.
If you do need a reactive variable for Apollo field policies, you can read it from Vue by subscribing manually:
```ts
import { makeVar } from '@apollo/client/cache'
-import { onScopeDispose, ref, shallowRef } from 'vue'
+import { onScopeDispose, shallowRef } from 'vue'
export function useApolloReactiveVar(rv: ReturnType>) {
const state = shallowRef(rv())
@@ -89,7 +90,7 @@ But unless you have a specific reason to keep state inside Apollo's cache layer
When you do go the Apollo local-state route, the two mechanisms are:
-- **Field policies with `read` functions.** Configure a type policy whose `read` returns the local value. Most flexible, no resolver overhead, recommended.
+- **Field policies with `read` functions.** Configure a type policy whose `read` returns the local value. Most flexible, and the recommended approach.
- **Local resolvers.** Implement resolvers for `@client` fields the same way you would for a server. Older approach; Apollo Client v4 still supports it but field policies are now preferred.
Both are documented in detail in the Apollo docs:
@@ -105,7 +106,7 @@ From a Vue Apollo perspective, both work the same: query as usual, and `@client`
|------|------|
| Component-local UI state (open/closed, active tab) | Vue ref |
| App-wide state shared across components (auth, theme) | Pinia store |
-| Server data with caching | `useQuery` |
+| Server data with caching | A query |
| Client-only field accessed through a GraphQL query | Apollo field policy with `read` |
| Client-only field accessed everywhere except GraphQL | Vue ref or Pinia store |
diff --git a/packages/docs/migration/compat.md b/packages/docs/migration/compat.md
index df8b425e..0e61637e 100644
--- a/packages/docs/migration/compat.md
+++ b/packages/docs/migration/compat.md
@@ -4,6 +4,12 @@
For a step-by-step migration that uses this layer, see the [Migration guide](/migration/guide). For a high-level summary of the v4 to v5 changes, see [What's changed in v5](/migration/whats-changed).
+::: warning Composables only
+`@vue/apollo-components` has no compat entry point. Its v5 rewrite changes props, slots and
+events with no shim, so those changes have to be made up front. See
+[Migrating v4 components](/migration/components).
+:::
+
## Purpose
Vue Apollo v5 introduced breaking changes to nearly every composable's call signature (variables moved into options, `mutate(variables)` became `mutate({ variables })`, the error type changed). Migrating a large codebase line-by-line in one shot is painful. The compat layer keeps the v4 surface compilable while you do the rest of the upgrade incrementally.
@@ -32,7 +38,7 @@ The compat module re-exports everything from the main entry, so a single find-re
| `useQuery` | `useQuery(doc)`, `useQuery(doc, vars)`, `useQuery(doc, vars, options)`, `useQuery(doc, undefined, options)` | Variables can be positional or in options. |
| `useSubscription` | Same 3-arg form as `useQuery` | Variables can be positional or in options. |
| `useMutation` | `useMutation(doc, options?)`. `mutate(variables, overrides)` keeps the v4 two-arg form. | The composable signature was already compatible; the call signature is wrapped. |
-| `useLazyQuery` | `useLazyQuery(doc, vars?, options?)`. `load(document?, variables?)` keeps v4's two-arg form and first-call-only semantics. | See [`useLazyQuery` notes](#uselazyquery) below. |
+| `useLazyQuery` | `useLazyQuery(doc, vars?, options?)`. `load(document?, variables?)` keeps v4's two-arg form and first-call-only semantics. | See [`useLazyQuery` notes](#uselazyquery-notes) below. |
### Return-shape wrapping
@@ -211,4 +217,4 @@ There is no deprecation timeline on the compat module itself, but every new feat
- [Migration guide](/migration/guide) is the prescriptive walkthrough.
- [What's changed in v5](/migration/whats-changed) is the high-level reference.
-- [TypeScript: Composable return-value shapes](/data/typescript#composable-return-value-shapes) compares v5 native composable shapes.
+- [TypeScript: Result shapes](/data/typescript#result-shapes) compares v5 native composable shapes.
diff --git a/packages/docs/migration/components.md b/packages/docs/migration/components.md
new file mode 100644
index 00000000..d8798e5a
--- /dev/null
+++ b/packages/docs/migration/components.md
@@ -0,0 +1,310 @@
+# 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`](/api/composable/).
+
+This page lists what moved.
+
+::: warning No compat layer for components
+[`@vue/apollo-composable/compat`](/migration/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
+`
+
+```
+
+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 `
` around its slot content, configurable with the `tag` prop. The v5 components render exactly what their slots return and nothing else.
+
+```vue-html
+
+
+ ...
+
+```
+
+Delete any `tag` prop, and check styling that relied on the wrapper. If you need one back, write it yourself:
+
+```vue-html
+
+
+ ...
+
+
+```
+
+::: tip Multiple roots
+Because the components are renderless, an [``](/api/components/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.
+:::
+
+## ``
+
+### Props
+
+| v4 | v5 |
+|---|---|
+| `query` (document or `gql => document`) | `query`, a document only. Use a `TypedDocumentNode` to get typed slots. |
+| `variables` | `variables`, typed from the document |
+| `skip` | `disabled` |
+| `fetchPolicy` | Same name |
+| `pollInterval` | Same name |
+| `debounce`, `throttle` (default `0`) | Same names, no default. Absent means no delay. |
+| `clientId` | Same name |
+| `update` (transform the data) | Removed. Transform inside the slot, or with a cache field policy. |
+| `notifyOnNetworkStatusChange`, `context`, `deep`, `prefetch` | Through `options`, where the option still exists in Apollo Client v4 |
+| `tag` | Removed, see above |
+| `options` | `options`, now the whole [`useQuery.Options`](/api/composable/@vue/namespaces/useQuery/interfaces/Options) object |
+
+`skip` to `disabled` is a rename only; the meaning is identical. See [Disabling queries](/data/queries#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:
+
+```vue-html
+
+
+
+
+
+```
+
+`#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 prop | v5 |
+|---|---|
+| `result.data` | `data` in `#data`, never `undefined` there. `result` in the default slot. |
+| `result.loading` | `loading`, in `#data` and in the default slot |
+| `result.error` | `error`, in `#error`, `#data` and the default slot. In `#data` it means the query failed but there is still something to show. |
+| `result.networkStatus` | `networkStatus`, default slot only |
+| `result.fullData` | Removed, along with the `update` prop it existed for |
+| `isLoading` | `loading` |
+| `gqlError` | Removed. Narrow with `CombinedGraphQLErrors.is(error)`, see [Error Handling](/data/error-handling#identifying-error-types). |
+| `times` | Removed |
+| `query` | On 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](/data/queries#two-ways-to-read-the-result) covers both modes.
+
+### Events
+
+| v4 | v5 |
+|---|---|
+| `@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](/data/queries#event-hooks).
+
+### Retaining results between variable changes
+
+v4's `` 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:
+
+```vue-html
+
+```
+
+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](/data/queries#keeping-previous-data).
+
+## ``
+
+### Props
+
+| v4 | v5 |
+|---|---|
+| `mutation` (document or `gql => document`) | `mutation`, a document only |
+| `variables` | `variables`, typed from the document |
+| `clientId` | Same name |
+| `optimisticResponse`, `update`, `refetchQueries`, `context` | Through `options`, typed as [`useMutation.Options`](/api/composable/@vue/namespaces/useMutation/interfaces/Options) |
+| `tag` | Removed |
+
+```vue-html
+
+
+```
+
+```vue-html
+
+
+```
+
+### `mutate()`
+
+[``](/api/components/ApolloMutation)'s `mutate` slot prop now takes an options object rather than bare variables, matching `useMutation`:
+
+```vue-html
+
+Add
+```
+
+```vue-html
+
+Add
+```
+
+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 ``. 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](/data/mutations#error-throwing-behavior).
+
+## ``
+
+[``](/api/components/ApolloSubscribeToMore) keeps its name and its placement: it still takes `document`, `variables` and `updateQuery`, and still has to sit inside an ``.
+
+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.
+
+```vue-html
+
+
+
+ ...
+
+
+```
+
+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:
+
+- [``](/api/components/ApolloSubscription) runs a standalone subscription, which v4 could only do through `this.$apollo.addSmartSubscription`. See [Subscriptions](/data/subscriptions).
+- [``](/api/components/ApolloFragment) reads a fragment from the cache. It is what makes the components usable with [data masking](/data/data-masking), where a component receives a masked object and has to unmask the fields it owns. See [Fragments](/data/fragments).
+
+## What has no component form
+
+- **Lazy queries.** See [Lazy Queries](/advanced/lazy-queries); `disabled` covers the "wait for variables" case.
+- **Suspense.** See [Suspense](/data/suspense).
+- **Aggregate loading counters.** `useQueryLoading` and friends are scoped per component instance, so they cannot see a child ``. See [Loading States](/advanced/loading-states).
+
+The two APIs share one client and one cache, so mixing them is expected. Reach for `
+
+
+
+
+
+
+ {{ item.message }}
+
+
+
+ Load more
+
+
+
+
+```
-Each call to `loadMore` passes the cursor from the previous page. Apollo merges the new items into the cached list using the `merge` function configured above.
+`#data` only renders once there is a result, so `data.feed.hasMore` and
+`data.feed.nextCursor` are safe to read without checking for one first.
+::::
+
+Each `fetchMore` call passes the cursor from the previous page. Apollo merges the new items into the cached list using the `merge` function configured above.
## Relay-style connections
@@ -157,13 +184,9 @@ The helper handles `edges`, `pageInfo`, and the standard cursor naming.
Usage:
-```vue twoslash
+:::: composition-api
+```vue
+
+
+
+
+
+
+ {{ edge.node.text }}
+
+
+
+ Load more
+
+
+
+
+```
+::::
## Inserting new items into a paginated list
@@ -220,6 +270,27 @@ The cache cannot tell which page a newly-created item belongs to, so a mutation
See [Cache Updates](/caching/cache-updates) for the insertion patterns.
+:::: components-api
+[``](/api/components/ApolloMutation) has props only for `mutation`,
+`variables` and `clientId`. `update`, `refetchQueries` and `optimisticResponse` all reach
+`useMutation` through the `options` prop, which takes the full
+[`useMutation.Options`](/api/composable/@vue/namespaces/useMutation/interfaces/Options)
+object:
+
+```vue-html
+
+
+ Post
+
+
+```
+::::
+
## Key arguments
If your paginated field also accepts filter or sort arguments, mark those as `keyArgs` so each filter value gets its own merged list:
diff --git a/packages/docs/pagination/offset-based.md b/packages/docs/pagination/offset-based.md
index f7a1ec44..136cc21e 100644
--- a/packages/docs/pagination/offset-based.md
+++ b/packages/docs/pagination/offset-based.md
@@ -45,13 +45,9 @@ The helper defines a `merge` function that concatenates pages as they arrive, so
## Loading more with `fetchMore`
-```vue twoslash
+:::: composition-api
+```vue
+
+
+
+
+
+
+ {{ item.message }}
+
+
+
+ Load more
+
+
+
+
+```
+::::
-`fetchMore` uses the original query and variables, overridden by what you pass. With the `offsetLimitPagination` helper installed, the new page is appended to the cached list automatically. `current.result.feed` updates to include every item received so far.
+`fetchMore` uses the original query and variables, overridden by what you pass. With the `offsetLimitPagination` helper installed, the new page is appended to the cached list automatically. The rendered list grows to include every item received so far.
## Key arguments
@@ -112,16 +135,11 @@ See [Apollo's `keyArgs` reference](https://www.apollographql.com/docs/react/pagi
## Reactive variables
-To use offset-based pagination with reactive variables (page-number UI), put `offset` and `limit` into refs:
+To use offset-based pagination with reactive variables (page-number UI), drive `offset` from a ref:
-```vue twoslash
+:::: composition-api
+```vue
+
+
+
+
+
+
+ {{ item.id }}
+
+
+
+
+
+
+ Previous
+
+
+ Next
+
+
+```
+
+This pattern replaces the page rather than appending. With `keepPreviousResult`, the
+previous page stays on screen while the new one loads, marked `isPreviousResult` so you can
+dim it until the new page lands. It is off by default. See
+[Keeping previous data](/data/queries#keeping-previous-data).
+::::
## Refreshing all pages
When you need to refresh the entire merged list (for example after a server-side reorder), refetch the original query:
+:::: composition-api
```ts
const { refetch } = useQuery(FEED_QUERY, {
variables: { offset: 0, limit: 10 },
@@ -166,6 +226,17 @@ const { refetch } = useQuery(FEED_QUERY, {
await refetch()
```
+::::
+
+:::: components-api
+```vue-html
+
+
+ Refresh
+
+
+```
+::::
The cache is rebuilt from the new response.
diff --git a/packages/docs/pagination/overview.md b/packages/docs/pagination/overview.md
index fc343eca..8eac5add 100644
--- a/packages/docs/pagination/overview.md
+++ b/packages/docs/pagination/overview.md
@@ -58,16 +58,11 @@ The `merge` function in the field policy decides how a new page combines with wh
## Loading the next page
+:::: composition-api
Vue Apollo's [`useQuery`](/api/composable/functions/useQuery) returns a `fetchMore` function for loading the next page:
-```vue twoslash
+```vue
+
+
+
+
+ Loading...
+
+
+
+
+ {{ item.message }}
+
+
+
+ Load more
+
+
+
+
+```
+
+The `variables` prop stays at the *first* page throughout. `fetchMore` does not change it.
+::::
When you call `fetchMore`, Apollo:
1. Sends a query with the new variables.
2. Merges the result with the cached value using the field policy's `merge` function.
-3. Notifies any active query that reads the field, including this one. The `current.result` ref updates with the merged list.
+3. Notifies any active query that reads the field, including this one. The rendered list updates with the merged value.
You typically want the `offsetLimitPagination` helper (or a cursor equivalent) configured for the field, otherwise `merge` defaults to replacing the cached value with the new page.
## With reactive variables
-If you keep pagination state in a Vue ref, `useQuery` re-executes when it changes. This is useful for "paginate by setting the page number" UIs:
+If you keep pagination state in a Vue ref, the query re-executes when it changes. This is useful for "paginate by setting the page number" UIs:
-```vue twoslash
+:::: composition-api
+```vue
+
+
+
+
+
+
+ {{ item.message }}
+
+
+
+
+
+
+ Previous
+
+
+ Next
+
+
+```
+
+For paginated lists you almost always want `keepPreviousResult`, so the list does not
+blink to empty between pages. It is off by default. See
+[Keeping previous data](/data/queries#keeping-previous-data).
+::::
::: tip `fetchMore` vs reactive variables
Use `fetchMore` when you want to grow an in-place list (infinite scroll, "load more" button). Use reactive variables when each page replaces the previous one (numbered pagination, "next page" navigation).
@@ -164,6 +229,27 @@ After a mutation adds or removes an item from a paginated list, the cache does n
For lists where order matters and the server controls it (chronological feeds, server-side sort), `refetchQueries` is usually more reliable.
+:::: components-api
+[``](/api/components/ApolloMutation) has props only for `mutation`,
+`variables` and `clientId`. Both `update` and `refetchQueries` reach `useMutation` through
+the `options` prop, which takes the full
+[`useMutation.Options`](/api/composable/@vue/namespaces/useMutation/interfaces/Options)
+object:
+
+```vue-html
+
+
+ Add
+
+
+```
+::::
+
## Next steps
- [Offset-based](/pagination/offset-based) details the offset/limit pattern.
diff --git a/packages/docs/scripts/checkApiFlavors.ts b/packages/docs/scripts/checkApiFlavors.ts
new file mode 100644
index 00000000..3849e0ca
--- /dev/null
+++ b/packages/docs/scripts/checkApiFlavors.ts
@@ -0,0 +1,125 @@
+/**
+ * Lints the `:::: -api` containers across the guide.
+ *
+ * An unbalanced container fails silently: markdown-it renders the stray marker as text and
+ * the page still builds. Headings are rejected inside a block because VitePress builds the
+ * page outline from the DOM, so a hidden heading scrolls nowhere for the other flavor.
+ */
+
+import fs from 'node:fs'
+import path from 'node:path'
+import process from 'node:process'
+import { fileURLToPath } from 'node:url'
+import { API_FLAVORS } from '../.vitepress/apiFlavors.ts'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const docsDir = path.resolve(__dirname, '..')
+
+const IGNORED_DIRS = new Set(['node_modules', '.vitepress', 'public'])
+
+/** Generated output. `api/index.md` is hand written, so `api` itself stays in scope. */
+const IGNORED_PATHS = new Set(['api/composable', 'api/components'])
+
+const FLAVORS = API_FLAVORS.map(flavor => flavor.value).join('|')
+
+// markdown-it-container opens at three colons and closes on a run at least as long, so the
+// opening length has to be carried to the close rather than assumed.
+const OPEN = new RegExp(`^(:{3,})\\s*(?:${FLAVORS})-api(?:\\s+\\S.*)?$`)
+const CLOSE = /^(:{3,})\s*$/
+// The marker run is greedy and the tail cannot start with a fence character, so there is
+// only one way to split a fence line.
+const FENCE = /^\s*(`+|~+)([^`~].*)?$/
+
+function* markdownFiles(dir: string): Generator {
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ if (entry.name.startsWith('.') || IGNORED_DIRS.has(entry.name)) {
+ continue
+ }
+ const full = path.join(dir, entry.name)
+ if (entry.isDirectory()) {
+ if (!IGNORED_PATHS.has(path.relative(docsDir, full))) {
+ yield* markdownFiles(full)
+ }
+ }
+ else if (entry.name.endsWith('.md')) {
+ yield full
+ }
+ }
+}
+
+const problems: string[] = []
+
+for (const file of markdownFiles(docsDir)) {
+ const relative = path.relative(docsDir, file)
+ const lines = fs.readFileSync(file, 'utf-8').split('\n')
+
+ let openedAt: number | null = null
+ let openLength = 0
+ let fence: { char: string, length: number } | null = null
+
+ lines.forEach((line, index) => {
+ const fenceMatch = FENCE.exec(line)
+ if (fenceMatch != null && fenceMatch[1]!.length >= 3) {
+ const marker = fenceMatch[1]!
+ if (fence == null) {
+ fence = { char: marker[0]!, length: marker.length }
+ }
+ else if (marker[0] === fence.char && marker.length >= fence.length && (fenceMatch[2] ?? '').trim() === '') {
+ fence = null
+ }
+ return
+ }
+ if (fence != null) {
+ return
+ }
+
+ const openMatch = OPEN.exec(line)
+ if (openMatch != null) {
+ const length = openMatch[1]!.length
+ if (length < 4) {
+ problems.push(`${relative}:${index + 1} opens a flavor block with ${length} colons; use four so it can wrap \`:::\` containers`)
+ }
+ if (openedAt != null) {
+ problems.push(`${relative}:${index + 1} opens a flavor block while one from line ${openedAt} is still open`)
+ }
+ openedAt = index + 1
+ openLength = length
+ return
+ }
+
+ const closeMatch = CLOSE.exec(line)
+ if (closeMatch != null) {
+ const length = closeMatch[1]!.length
+ if (openedAt == null) {
+ if (length >= 4) {
+ problems.push(`${relative}:${index + 1} closes a flavor block that was never opened`)
+ }
+ return
+ }
+ // A shorter run belongs to a nested `::: tip`, exactly as markdown-it resolves it.
+ if (length >= openLength) {
+ openedAt = null
+ }
+ return
+ }
+
+ if (openedAt != null && /^#{1,6}\s/.test(line)) {
+ problems.push(`${relative}:${index + 1} puts a heading inside a flavor block; move it above the \`::::\``)
+ }
+ })
+
+ if (openedAt != null) {
+ problems.push(`${relative}:${openedAt} opens a flavor block that is never closed`)
+ }
+}
+
+if (problems.length > 0) {
+ console.error(`Found ${problems.length} API flavor problem(s):\n`)
+ for (const problem of problems) {
+ console.error(` ${problem}`)
+ }
+ process.exitCode = 1
+}
+else {
+ console.log('API flavor containers are balanced.')
+}
diff --git a/packages/docs/scripts/generateComponentApi.ts b/packages/docs/scripts/generateComponentApi.ts
new file mode 100644
index 00000000..a133800e
--- /dev/null
+++ b/packages/docs/scripts/generateComponentApi.ts
@@ -0,0 +1,240 @@
+/**
+ * Generates the API reference for `@vue/apollo-components`.
+ *
+ * api-extractor and typedoc both work off `.d.ts`, which for an SFC is the compiler's
+ * `__VLS_` shape rather than anything readable. `vue-component-meta` runs the same
+ * analysis `vue-tsc` does, so props, events and slots come out with their JSDoc attached.
+ */
+
+import fs from 'node:fs'
+import path from 'node:path'
+import process from 'node:process'
+import { fileURLToPath } from 'node:url'
+import { createChecker } from 'vue-component-meta'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const docsDir = path.resolve(__dirname, '..')
+const componentsDir = path.resolve(docsDir, '../vue-apollo-components')
+const outDir = path.join(docsDir, 'api/components')
+
+const COMPONENTS = [
+ 'ApolloQuery',
+ 'ApolloMutation',
+ 'ApolloSubscription',
+ 'ApolloSubscribeToMore',
+ 'ApolloFragment',
+] as const
+
+/**
+ * The virtual file the SFC compiles to aliases its imports, so a composable namespace
+ * prints as `u.Options`. Each component imports exactly one composable, which makes the
+ * expansion unambiguous.
+ */
+function composableOf(source: string): string | undefined {
+ return source.match(/import \{ (use\w+) \} from '@vue\/apollo-composable'/)?.[1]
+}
+
+/**
+ * `NoInfer` marks a prop as a non-inference site. It says nothing about what may be passed,
+ * so it would only puzzle a reader of the table.
+ */
+function dropNoInfer(type: string): string {
+ return type.replace(/\bNoInfer<((?:[^<>]|<[^<>]*>)+)>/g, '$1')
+}
+
+/** The rolled-up barrel abbreviates its exports; `R` is the public `RenameKey`. */
+const BARREL_ALIASES: Record = { R: 'RenameKey' }
+
+/** Short real namespaces the import-alias rewrite below must not swallow. */
+const NOT_AN_ALIAS = /^(?:Map|Set|JSX|Vue|Ref)\.$/
+
+function resolveAliases(type: string, source: string): string {
+ const resolved = dropNoInfer(type)
+ .replace(/\b([A-Z])(?=<)/g, (match, alias: string) => BARREL_ALIASES[alias] ?? match)
+ const composable = composableOf(source)
+ if (composable == null) {
+ return resolved
+ }
+ return resolved.replace(/\b[A-Za-z_$]{1,3}\.(?=[A-Z])/g, match =>
+ NOT_AN_ALIAS.test(match) ? match : `${composable}.`)
+}
+
+/**
+ * Inlines a local `type X = ...` declared in the SFC.
+ *
+ * `vue-component-meta` prints such an alias by name, and the reader has no file to look it
+ * up in. Long declarations stay abbreviated; expanding them helps nobody.
+ */
+function resolveLocalAliases(type: string, source: string): string {
+ let resolved = type
+
+ for (let pass = 0; pass < 2; pass++) {
+ resolved = resolved.replace(/\b[A-Z][\w$]*\b/g, (name) => {
+ const declaration = new RegExp(`^type ${name} =([\\s\\S]*?)\\n(?=\\n|type |const |interface )`, 'm').exec(source)
+ const inlined = declaration?.[1]
+ ?.replace(/\s+/g, ' ')
+ .replace(/<\s+/g, '<')
+ .replace(/\s+>/g, '>')
+ .replace(/\s+,/g, ',')
+ .trim()
+ return inlined != null && inlined.length <= 160 ? inlined : name
+ })
+ }
+
+ return resolved
+}
+
+/** Placeholder for a table cell with nothing to say, kept visually quieter than a dash. */
+const EMPTY = ' '
+
+function cell(value: string | undefined) {
+ if (!value) {
+ return EMPTY
+ }
+ // A table cell cannot hold a real paragraph break, so keep it as a visible one.
+ return value
+ .replace(/\|/g, '\\|')
+ .replace(/\r?\n\s*\n/g, '
')
+ .replace(/\r?\n/g, ' ')
+}
+
+function code(value: string | undefined) {
+ return value ? `\`${cell(value)}\`` : EMPTY
+}
+
+interface SlotProp { name: string, type: string }
+
+/** `vue-component-meta` reports a slot that takes nothing as `any`. */
+function takesNoProps(slot: { type?: string }): boolean {
+ return slot.type == null || slot.type === 'any'
+}
+
+function slotProps(schema: unknown): SlotProp[] {
+ if (typeof schema !== 'object' || schema == null) {
+ return []
+ }
+ const resolved = schema as { kind?: string, schema?: Record }
+ if (resolved.kind !== 'object' || resolved.schema == null) {
+ return []
+ }
+ return Object.entries(resolved.schema).map(([name, value]) => ({
+ name,
+ type: value.type ?? 'unknown',
+ }))
+}
+
+function render(name: string, meta: ReturnType['getComponentMeta']>, source: string) {
+ const expand = (type: string) => resolveAliases(resolveLocalAliases(type, source), source)
+ const composable = composableOf(source)
+ const lines: string[] = [
+ `# ${name}`,
+ '',
+ '',
+ '',
+ ]
+
+ const props = meta.props.filter(prop => !prop.global)
+ if (props.length > 0) {
+ lines.push('## Props', '', '| Name | Type | Default | Description |', '| --- | --- | --- | --- |')
+ for (const prop of props) {
+ const required = prop.required ? ' *(required)*' : ''
+ lines.push(
+ `| \`${prop.name}\`${required} | ${code(expand(prop.type))} | ${code(prop.default === 'undefined' ? undefined : prop.default)} | ${cell(prop.description)} |`,
+ )
+ }
+ lines.push('')
+ }
+
+ if (meta.events.length > 0) {
+ lines.push('## Events', '', '| Name | Payload |', '| --- | --- |')
+ for (const event of meta.events) {
+ lines.push(`| \`${event.name}\` | ${code(expand(event.type))} |`)
+ }
+ lines.push('')
+ }
+
+ if (meta.slots.length > 0) {
+ lines.push('## Slots', '')
+ for (const slot of meta.slots) {
+ lines.push(`### \`#${slot.name}\``, '')
+ if (slot.description) {
+ lines.push(slot.description, '')
+ }
+ const scoped = slotProps(slot.schema)
+ if (scoped.length > 0) {
+ lines.push('| Slot prop | Type |', '| --- | --- |')
+ for (const prop of scoped) {
+ lines.push(`| \`${prop.name}\` | ${code(expand(prop.type))} |`)
+ }
+ }
+ else if (takesNoProps(slot)) {
+ lines.push('No slot props.')
+ }
+ else {
+ // A union of shapes has no single table, so name the type rather than claim none.
+ const type = expand(slot.type)
+ lines.push(`Receives ${code(type)}.`)
+ if (composable != null && type.includes(`${composable}.Current`)) {
+ lines.push(
+ '',
+ `Every field of [\`${composable}.Current\`](/api/composable/@vue/namespaces/${composable}/interfaces/Current) is present, so \`resultState\` narrows \`result\` here exactly as it does in script.`,
+ )
+ }
+ }
+ lines.push('')
+ }
+ }
+
+ // `vue-component-meta` cannot read `defineExpose` from a generic SFC, and the exposed
+ // surface is the composable's result anyway, so link it rather than duplicate it.
+ if (composable != null) {
+ lines.push(
+ '## Exposed',
+ '',
+ `A template ref receives the full [\`${composable}.Result\`](/api/composable/@vue/namespaces/${composable}/interfaces/Result) surface, with refs unwrapped.`,
+ '',
+ '```vue',
+ '',
+ ` <${name} ref="apollo" ... />`,
+ '',
+ '```',
+ '',
+ )
+ }
+
+ return `${lines.join('\n').trimEnd()}\n`
+}
+
+function generate() {
+ const checker = createChecker(path.join(componentsDir, 'tsconfig.lib.json'), {
+ forceUseTs: true,
+ schema: { ignore: [] },
+ })
+
+ fs.mkdirSync(outDir, { recursive: true })
+
+ for (const name of COMPONENTS) {
+ const file = path.join(componentsDir, 'src', `${name}.vue`)
+ const source = fs.readFileSync(file, 'utf-8')
+ const meta = checker.getComponentMeta(file)
+ fs.writeFileSync(path.join(outDir, `${name}.md`), render(name, meta, source))
+ console.log(` Generated: ${path.relative(docsDir, path.join(outDir, `${name}.md`))}`)
+ }
+
+ const index = [
+ '# Components API',
+ '',
+ '',
+ '',
+ ...COMPONENTS.map(name => `- [${name}](./${name})`),
+ '',
+ ].join('\n')
+ fs.writeFileSync(path.join(outDir, 'index.md'), index)
+}
+
+console.log('Generating @vue/apollo-components API reference...')
+generate()
+
+if (process.exitCode == null) {
+ console.log(`Total: ${COMPONENTS.length} components`)
+}
diff --git a/packages/docs/ssr/nuxt.md b/packages/docs/ssr/nuxt.md
index 9dbc3869..a12b387c 100644
--- a/packages/docs/ssr/nuxt.md
+++ b/packages/docs/ssr/nuxt.md
@@ -89,10 +89,11 @@ What this does:
- The `app:rendered` hook serializes the cache into Nuxt's payload after server rendering.
- `ssrForceFetchDelay` lets cached queries revalidate from the network shortly after hydration, useful for slightly stale data.
-## Step 3: Use the composables
+## Step 3: Fetch some data
In any page or component:
+:::: composition-api
```vue
+
+
+
+
+ {{ data.company.ceo }}
+
+
+
+```
+
+`` registers an `onServerPrefetch` hook automatically, so Nuxt waits for the
+data before rendering the page. The result is in HTML, and the client hydrates without a
+re-fetch.
+
+Nuxt auto-imports do not cover this package, so import the components explicitly.
+::::
## Awaiting queries with Suspense
@@ -137,6 +169,16 @@ const { current } = await useQuery(gql`
Nuxt wraps pages in `` automatically, so this works without extra setup. See [Suspense](/data/suspense) for the full pattern.
+:::: components-api
+::: warning Composition API only
+`` cannot suspend, so a page built from it renders through its slots rather
+than blocking. Nuxt still waits for the data.
+
+Use `await useQuery(...)` in `
+
+
+
+
+```
+
+The check has to live in `
+
+
+
+
+ Loading…
+
+
+ {{ error.message }}
+
+ Retry
+
+
+
+