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:
type Subscription {
messageAdded(channelId: ID!): Message!
}
On the client, subscription queries look just like any other kind of operation:
subscription onMessageAdded($channelId: ID!) {
messageAdded(channelId: $channelId) {
id
text
}
}
The response sent to the client looks as follows:
{
"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 and setting up GraphQL subscriptions for the server 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:
- 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.
- 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, or enjoy subscriptions set up out of the box if you are using a GraphQL backend as a service like Graphcool.
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 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. First, install:
npm install graphql-ws
Then initialize a GraphQL web socket link:
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:
import { HttpLink, split } from "@apollo/client/core"
import { GraphQLWsLink } from "@apollo/client/link/subscriptions"; // <-- This one uses graphql-ws
import { createClient } from "graphql-ws";
import { getMainDefinition } from "@apollo/client/utilities"
// 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 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 because your server still uses that protocol, instead of installing graphql-ws, install:
npm install subscriptions-transport-ws
And then initialize a GraphQL web socket link:
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:
<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:
<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:
<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:
<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:
const variables = ref({
channelId: "abc"
})
const { result } = useSubscription(
gql`
subscription onMessageAdded($channelId: ID!) {
messageAdded(channelId: $channelId) {
id
text
}
}
`,
variables
)
With a reactive object:
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):
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
:
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:
const { result } = useSubscription(
gql`
subscription onMessageAdded($channelId: ID!) {
messageAdded(channelId: $channelId) {
id
text
}
}
`,
null,
() => ({
fetchPolicy: "no-cache"
})
)
See the API Reference for all the possible options.
Disable a subscription
You can disable and re-enable a subscription with the enabled
option:
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
:
const { loading, error } = useSubscription(...)
Event hooks
onResult
This is called when a new result is received from the server:
const { onResult } = useSubscription(...)
onResult((result, context) => {
console.log(result.data)
})
onError
This is triggered when an error occurs:
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:
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 (modified a little bit to have a variable):
<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
:
<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:
subscribeToMore({
// options...
})
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:
<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:
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.
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.