Subscriptions
villus
handles subscriptions with the useSubscription
function.
To add support for subscriptions you need to add the handleSubscriptions
plugin to the useClient
plugin list, which in turn will call your subscription client. The plugin expects a function that returns an object that follows the observable spec to be returned, this function is called a subscription forwarder.
You can use graphql-ws
package for your subscriptions implemented with websockets protocol, so one way to build your subscription forwarder is like this:
vue<script setup>
import { useClient } from 'villus';
import { createClient } from 'graphql-ws';
const wsClient = createClient({
url: 'ws://localhost:9005/graphql',
});
const subscriptionsHandler = handleSubscriptions(operation => {
return {
subscribe: obs => {
wsClient.subscribe(
{
query: operation.query,
variables: operation.variables,
},
obs,
);
return {
unsubscribe: () => {
// No OP
},
};
},
};
});
const client = useClient({
url: 'http://localhost:4000/graphql',
// Install the subscriptions handler
use: [subscriptionsHandler, ...defaultPlugins()],
});
</script>
Executing Subscriptions
The useSubscription
function has a similar API as it exposes a data
property that you can watch
vue<template>
<ul>
<li v-for="message in messages">{{ message }}</li>
</ul>
</template>
<script setup>
import { watch, ref } from 'vue';
import { useSubscription } from 'villus';
const NewMessages = `
subscription NewMessages {
newMessages {
id
from
message
}
}
`;
const messages = ref([]);
useSubscription({ query: NewMessages }, ({ data, error }) => {
// do stuff with incoming data
if (data) {
messages.value.push(...data.newMessages);
}
// Handler errors
if (error) {
}
});
</script>
Passing Variables
You can pass variables to subscriptions by passing an object containing both query
and variables
as the first argument:
vue<script setup>
import { useSubscription } from 'villus';
const NewMessages = `
subscription ConversationMessages ($id: ID!) {
conversation(id: $id) {
id
from
message
}
}
`;
const { data } = useSubscription({
query: NewMessages,
variables: { id: 1 },
});
</script>
Handling Subscription Data
The previous examples are not very useful, as usually you would like to be able to use the data
as a continuous value rather than a reference to the last received value, that is why you can pass a custom mapper or a reducer as the second argument to the useSubscription
function. This callback function receives the new result from the subscription as the first argument, and the reduced data if available as the second argument. You can use it as either a mapper or a reducer depending on what you return from the function.
to understand the difference between a mapper and a reducer check the next couple of examples.
Mapping data
In the following example, the passed function only extracts the lastMessage
field out from the response data. That means it only returns a mapped version of the last result recieved.
vue<script setup>
import { useSubscription } from 'villus';
const LastMessage = `
subscription LastMessage {
lastMessage {
id
from
message
}
}
`;
// rename data to be more descriptive of its usage
const { data: lastMessage } = useSubscription(
{
query: LastMessage,
},
({ data }) => {
// remember that data can be null
return data?.lastMessage;
},
);
// anywhere
lastMessage.value; // { id: 1, from: 'someone', message: 'hello' }
</script>
This is useful for subscriptions that represent a live state for something on your API. You are not limited to mapping the results in a specific way, for example if you want to return a boolean, you can do so:
vue<script setup>
import { useSubscription } from 'villus';
const UnreadMessages = `
subscription UnreadMessages {
unreadMessages {
id
}
}
`;
// rename data to be more descriptive of its usage
const { data: hasUnread } = useSubscription(
{
query: UnreadMessages,
},
({ data }) => {
return data?.unreadMessages.length > 1;
},
);
// anywhere
hasUnread.value; // true or false
</script>
So it is completely is up to you how to map the data and make them useful to your needs.
Reducing data
The reducer is a subscription handler that aggregates past and future results into a single value. The aggregated value will become the data
returned from useSubscription
.
Here is the last example with a custom reducer:
vue<script setup>
import { useSubscription } from 'villus';
const NewMessages = `
subscription NewMessages {
newMessages {
id
from
message
}
}
`;
// rename data to make it more clear
const { data: messages } = useSubscription(
{
query: NewMessages,
},
({ data, error }, oldValue) => {
// old value is nullable
oldValue = oldValue || [];
// in case of error just return the last value
if (!data || error) {
return oldValue;
}
// combine old and incoming messages
return [...oldValue, response.data.newMessages];
},
);
messages.value; // { id, from, message }[]
</script>
The function here acts as a reducer for the incoming data, whenever a new response is received it will be passed to reduceMessages
function as the second argument, the first argument will be the previous value. This makes it more compact than having to declare state or watching over the data yourself.
tip
Keep in mind that initially, we have null
for the initial value so we needed to provide a fallback for that.
Pausing subscriptions
Similar to queries, subscriptions could also be paused by passing a paused
value to useSubscription
.
vue<script setup>
import { ref } from 'vue';
import { useSubscription } from 'villus';
const NewMessages = `
subscription ConversationMessages ($id: ID!) {
conversation(id: $id) {
id
from
message
}
}
`;
const paused = ref(false);
const { data } = useSubscription({
query: NewMessages,
variables: { id: 1 },
paused,
});
// pause the subscription
paused.value = true;
// resume the subscription
paused.value = false;
</script>
Note that pausing or unpausing doesn’t sever the established connection (if websocket implementation is used), all it does is ignore the incoming values.