Files
apollo/packages/docs/src/guide-composable/subscription.md
T
2025-03-08 21:12:47 +01:00

644 lines
16 KiB
Markdown

# Subscriptions
In addition to fetching data using queries and modifying data using mutations, the GraphQL spec supports a third operation type, called `subscription`.
GraphQL subscriptions are a way to push data from the server to the clients that choose to listen to real time messages from the server. Subscriptions are similar to queries in that they specify a set of fields to be delivered to the client, but instead of immediately returning a single answer, a result is sent every time a particular event happens on the server.
A common use case for subscriptions is notifying the client side about particular events, for example the creation of a new object, updated fields and so on.
## Overview
GraphQL subscriptions have to be defined in the schema, just like queries and mutations:
```graphql
type Subscription {
messageAdded(channelId: ID!): Message!
}
```
On the client, subscription queries look just like any other kind of operation:
```graphql
subscription onMessageAdded($channelId: ID!) {
messageAdded(channelId: $channelId) {
id
text
}
}
```
The response sent to the client looks as follows:
```json
{
"data": {
"messageAdded": {
"id": "123",
"text": "Hello!"
}
}
}
```
In the above example, the server is written to send a new result every time a message is added for a specific channel. Note that the code above only defines the GraphQL subscription in the schema. Read [setting up subscriptions on the client](#client-setup) and [setting up GraphQL subscriptions for the server](https://www.apollographql.com/docs/graphql-subscriptions) to learn how to add subscriptions to your app.
### When to use subscriptions
In most cases, intermittent polling or manual refetching are actually the best way to keep your client up to date. So when is a subscription the best option? Subscriptions are especially useful if:
1. The initial state is large, but the incremental change sets are small. The starting state can be fetched with a query and subsequently updated through a subscription.
2. You care about low-latency updates in the case of specific events, for example in the case of a chat application where users expect to receive new messages in a matter of seconds.
A future version of Apollo or GraphQL might include support for live queries, which would be a low-latency way to replace polling, but at this point general live queries in GraphQL are not yet possible outside of some relatively experimental setups.
## Client setup
In this article, we'll explain how to set it up on the client, but you'll also need a server implementation. You can [read about how to use subscriptions with a JavaScript server](https://www.apollographql.com/docs/graphql-subscriptions/setup), or enjoy subscriptions set up out of the box if you are using a GraphQL backend as a service like [Graphcool](https://www.graph.cool/docs/tutorials/worldchat-subscriptions-example-ui0eizishe/).
The GraphQL spec does not define a specific protocol for sending subscription requests. The first popular JavaScript library to implement subscriptions over WebSocket is called *subscriptions-transport-ws*. This library is no longer actively maintained. Its successor is a library called *graphql-ws*. The two libraries do not use the same WebSocket subprotocol, so you need to make sure that your server and clients all use the same library.
Apollo Client supports both *graphql-ws* and *subscriptions-transport-ws*. Apollo [documentation](https://www.apollographql.com/docs/react/data/subscriptions/#choosing-a-subscription-library) suggest to use the newer library *graphql-ws*, but in case you need it, here its explained how to do it with both.
### The new library: **graphql-ws**
Let's look at how to add support for this transport to Apollo Client using a link set up for newest library [graphql-ws](https://github.com/enisdenjo/graphql-ws). First, install:
```bash
npm install graphql-ws
```
Then initialize a GraphQL web socket link:
```js
import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
import { createClient } from 'graphql-ws'
const wsLink = new GraphQLWsLink(
createClient({
url: 'ws://localhost:4000/graphql',
})
)
```
We need to either use the `GraphQLWsLink` or the `HttpLink` depending on the operation type:
```js
import { HttpLink, split } from '@apollo/client/core'
import { GraphQLWsLink } from '@apollo/client/link/subscriptions' // <-- This one uses graphql-ws
import { getMainDefinition } from '@apollo/client/utilities'
import { createClient } from 'graphql-ws'
// Create an http link:
const httpLink = new HttpLink({
uri: 'http://localhost:3000/graphql'
})
// Create a GraphQLWsLink link:
const wsLink = new GraphQLWsLink(
createClient({
url: 'ws://localhost:5000/',
})
)
// using the ability to split links, you can send data to each link
// depending on what kind of operation is being sent
const link = split(
// split based on operation type
({ query }) => {
const definition = getMainDefinition(query)
return (
definition.kind === 'OperationDefinition'
&& definition.operation === 'subscription'
)
},
wsLink,
httpLink
)
// Create the apollo client with cache implementation.
const apolloClient = new ApolloClient({
link,
cache: new InMemoryCache(),
})
```
The apollo client is the one that will be provided to the vue app, see the [setup section](https://v4.apollo.vuejs.org/guide-composable/setup.html) for more details.
Now, queries and mutations will go over HTTP as normal, but subscriptions will be done over the websocket transport.
### The old library: **subscriptions-transport-ws**
If you need to use [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws) because your server still uses that protocol, instead of installing graphql-ws, install:
```bash
npm install subscriptions-transport-ws
```
And then initialize a GraphQL web socket link:
```js
import { WebSocketLink } from '@apollo/client/link/ws' // <-- This one uses subscriptions-transport-ws
const wsLink = new WebSocketLink({
uri: `ws://localhost:5000/`,
options: {
reconnect: true
}
})
```
The rest of the configuration (creating a httpLink and link) is the same as described above for graphql-ws.
## useSubscription
The easiest way to add live data to your UI is using the `useSubscription` composition function. This lets you continuously receive updates from your server to update a `Ref` or a reactive object, thus re-rendering your component. One thing to note, subscriptions are just listeners, they don't request any data when first connected : they only open up a connection to get new data.
Start by importing `useSubscription` in your component:
```vue{2}
<script>
import { useSubscription } from "@vue/apollo-composable"
export default {
setup() {
// Data & Logic here...
}
}
</script>
```
We can then pass a GraphQL document as the first parameter and retrieve the `result` ref:
```vue{6-13}
<script>
import { useSubscription } from "@vue/apollo-composable"
export default {
setup() {
const { result } = useSubscription(gql`
subscription onMessageAdded {
messageAdded {
id
text
}
}
`)
}
}
</script>
```
We can then `watch` the result as new data is received:
```vue{2,16-20}
<script>
import { watch } from "vue"
import { useSubscription } from "@vue/apollo-composable"
export default {
setup() {
const { result } = useSubscription(gql`
subscription onMessageAdded {
messageAdded {
id
text
}
}
`)
watch(
result,
data => {
console.log("New message received:", data.messageAdded)
},
{
lazy: true // Don't immediately execute handler
}
)
}
}
</script>
```
For example, we could display the list of messages as we receive them:
```vue{2,7,19,24-26,31-39}
<script>
import { watch, ref } from "vue"
import { useSubscription } from "@vue/apollo-composable"
export default {
setup() {
const messages = ref([])
const { result } = useSubscription(gql`
subscription onMessageAdded {
messageAdded {
id
text
}
}
`)
watch(
result,
data => {
messages.value.push(data.messageAdded)
},
{
lazy: true // Don't immediately execute handler
}
)
return {
messages
}
}
}
</script>
<template>
<div>
<ul>
<li v-for="message of messages" :key="message.id">
{{ message.text }}
</li>
</ul>
</div>
</template>
```
### Variables
We can pass variables in the 2nd parameter. Just like `useQuery`, it can either be an object, a `Ref`, a reactive object or a function that will be made reactive.
With a ref:
```js
const variables = ref({
channelId: 'abc'
})
const { result } = useSubscription(
gql`
subscription onMessageAdded($channelId: ID!) {
messageAdded(channelId: $channelId) {
id
text
}
}
`,
variables
)
```
With a reactive object:
```js
const variables = reactive({
channelId: 'abc'
})
const { result } = useSubscription(
gql`
subscription onMessageAdded($channelId: ID!) {
messageAdded(channelId: $channelId) {
id
text
}
}
`,
variables
)
```
With a function (which will automatically be made reactive):
```js
const channelId = ref('abc')
const { result } = useSubscription(
gql`
subscription onMessageAdded($channelId: ID!) {
messageAdded(channelId: $channelId) {
id
text
}
}
`,
() => ({
channelId: channelId.value
})
)
```
### Options
Similar to the variables, you can pass options to the third parameter of `useSubscription`:
```js
const { result } = useSubscription(
gql`
subscription onMessageAdded($channelId: ID!) {
messageAdded(channelId: $channelId) {
id
text
}
}
`,
null,
{
fetchPolicy: 'no-cache'
}
)
```
It can also be a reactive object, or a function that will automatically be made reactive:
```js
const { result } = useSubscription(
gql`
subscription onMessageAdded($channelId: ID!) {
messageAdded(channelId: $channelId) {
id
text
}
}
`,
null,
() => ({
fetchPolicy: 'no-cache'
})
)
```
See the [API Reference](../api/use-subscription) for all the possible options.
### Disable a subscription
You can disable and re-enable a subscription with the `enabled` option:
```js
const enabled = ref(false)
const { result } = useSubscription(
gql`
...
`,
null,
() => ({
enabled: enabled.value
})
)
function enableSub() {
enabled.value = true
}
```
### Subscription status
You can retrieve the loading and error stats from `useSubscription`:
```js
const { loading, error } = useSubscription(/* ... */)
```
### Event hooks
#### onResult
This is called when a new result is received from the server:
```js
const { onResult } = useSubscription(/* ... */)
onResult((result, context) => {
console.log(result.data)
})
```
#### onError
This is triggered when an error occurs:
```js
import { logErrorMessages } from '@vue/apollo-util'
const { onError } = useSubscription(/* ... */)
onError((error, context) => {
logErrorMessages(error)
})
```
### Update the cache
Using `onResult`, you can update the Apollo cache with the new data:
```js
const { onResult } = useSubscription(/* ... */)
onResult((result, { client }) => {
const query = {
query: gql`query getMessages ($channelId: ID!) {
messages(channelId: $channelId) {
id
text
}
}`,
variables: {
channelId: '123',
},
}
// Read the query
let data = client.readQuery(query)
// Update cached data
data = {
...data,
messages: [...data.messages, result.data.messageAdded],
}
// Write back the new result for the query
client.writeQuery({
...query,
data,
})
})
```
## subscribeToMore
With GraphQL subscriptions your client will be alerted on push from the server and you should choose the pattern that fits your application the most:
- Use it as a notification and run any logic you want when it fires, for example alerting the user or refetching data
- Use the data sent along with the notification and merge it directly into the store (existing queries are automatically notified)
With `subscribeToMore`, you can easily do the latter.
`subscribeToMore` is a function available on every query created with `useQuery`. It works just like `fetchMore`, except that the update function gets called every time the subscription returns, instead of only once.
Let's take the query from our previous example component from the section on [mutations](./mutation) (modified a little bit to have a variable):
```vue
<script>
const MESSAGES = gql`
query getMessages($channelId: ID!) {
messages(channelId: $channelId) {
id
text
}
}
`
export default {
props: ["channelId"],
setup(props) {
// Messages list
const { result } = useQuery(MESSAGES, () => ({
channelId: props.channelId
}))
const messages = computed(() => result.value?.messages ?? [])
return {
messages
}
}
}
</script>
```
Now let's add the subscription to this query.
Retrieve the `subscribeToMore` function from `useQuery`:
```vue{16,21}
<script>
const MESSAGES = gql`
query getMessages($channelId: ID!) {
messages(channelId: $channelId) {
id
text
}
}
`
export default {
props: ["channelId"],
setup(props) {
// Messages list
const { result, subscribeToMore } = useQuery(MESSAGES, () => ({
channelId: props.channelId
}))
const messages = computed(() => result.value?.messages ?? [])
subscribeToMore()
return {
messages
}
}
}
</script>
```
It expects either an object or a function that will automatically be reactive:
```js
subscribeToMore({
// options...
})
```
```js
subscribeToMore(() => ({
// options...
}))
```
In the latter case, the subscription will automatically restart if the options change.
You can now put a GraphQL document with the relevant subscription, with variables if necessary:
```vue{21-33}
<script>
const MESSAGES = gql`
query getMessages($channelId: ID!) {
messages(channelId: $channelId) {
id
text
}
}
`
export default {
props: ["channelId"],
setup(props) {
// Messages list
const { result, subscribeToMore } = useQuery(MESSAGES, () => ({
channelId: props.channelId
}))
const messages = computed(() => result.value?.messages ?? [])
subscribeToMore(() => ({
document: gql`
subscription onMessageAdded($channelId: ID!) {
messageAdded(channelId: $channelId) {
id
text
}
}
`,
variables: {
channelId: props.channelId
}
}))
return {
messages
}
}
}
</script>
```
Now that the subscription is added to the query, we need to tell Apollo Client how to update the query result with the `updateQuery` option:
```js{13-16}
subscribeToMore(() => ({
document: gql`
subscription onMessageAdded($channelId: ID!) {
messageAdded(channelId: $channelId) {
id
text
}
}
`,
variables: {
channelId: props.channelId
},
updateQuery: (previousResult, { subscriptionData }) => {
const tmp = [...previousResult]
tmp.messages.push(subscriptionData.data.messageAdded)
return tmp
}
}))
```
## Authentication over WebSocket
In many cases it is necessary to authenticate clients before allowing them to receive subscription results. To do this, the `SubscriptionClient` constructor accepts a `connectionParams` field, which passes a custom object that the server can use to validate the connection before setting up any subscriptions.
```js
import { WebSocketLink } from '@apollo/client/link/ws'
const wsLink = new WebSocketLink({
uri: `ws://localhost:5000/`,
options: {
reconnect: true,
connectionParams: {
authToken: user.authToken,
},
}
})
```
::: tip
You can use `connectionParams` for anything else you might need, not only authentication, and check its payload on the server side with [SubscriptionsServer](https://www.apollographql.com/docs/graphql-subscriptions/authentication).
:::