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.