Modeling an Instagram Clone: GraphQL Subscriptions

Across our last four articles in this series, we explored many GraphQL-related concepts.

We went from designing GraphQL schema to adding authentication and authorization. We integrated Apollo Client which allows the app to send authenticated GraphQL requests to the server. Finally, with Apollo in place, we were able to fetch necessary data to render a feed, showing posts from users someone is following.

In this article, we’re going to learn about GraphQL subscriptions. Since Dgraph’s GraphQL API has this feature built-in; it’s really simple to use them within your app.

Here are the list of topics for today:

If you want, you can take a look at the previous articles in this series that follow up to this one:

You can find all the code changes for today’s article here.

What are GraphQL subscriptions?

Subscriptions are what make a GraphQL API capable of serving real-time data to its clients. An application that uses GraphQL subscriptions enables it to have it’s data updated without the need for polling for changes in the data. A subscription works simply: describe the data you want the client to subscribe to and when that data changes, the server will send the updates to the client automatically.

Since it’s GraphQL, the client will of course define what data it wants from the server through a subscription query.

Difference with normal GraphQL queries

For subscriptions to work, there needs to be a persistent connection between the client and the server. This is usually via WebSocket, as we’ll soon see when we get to the code. This is in contrast with normal GraphQL queries we’ve seen so far. With normal GraphQL queries, you just send a request whenever you need to.

Subscriptions are also autonomous, depending on events unlike normal queries. When these events occur, the server will then execute the subscription query. It won’t dispatch the query without these events.

For example, consider an app that tracks the number of daily COVID cases. This type of data is constantly changing, so you need a way to make sure that your app (the client) always has the latest information. You’d “subscribe” to the relevant databases across the world. In the event of new records, the servers will push the latest data your client’s way and your app will render the latest COVID statistics.

To know more about subscriptions, check out our subscription docs and our blog post on how GraphQL subscriptions work by building a NextJS app. You’ll get more hands-on familiarity with it as you go along with me today.

Why WebSocket and not HTTP?

As I said earlier, subscriptions make use of WebSockets for communication. Subscriptions maintain a persistent, stateful connection between the client and the server. The client doesn’t immediately request anything and the server doesn’t respond either. The server only pushes data when an event occurs that the client explicitly specified via a subscription query. For that, the server needs to know the subscription query and maintain a steady connection with the client.

Unlike HTTP, WebSockets offer a bi-directional way of communication between two entities. It’s low overhead and scalable when it comes to real-time data transfer. WebSockets, by design, are suitable for maintaining a persistent co

Enabling subscription

In order to enable subscription, you need to make some changes to both your server and client side code and configuration.

Enabling subscription in Dgraph GraphQL API

In Dgraph, you can enable subscription on any of your types by just appending the @withSubscription directive to it. Notice the following where we attach it to our User type definition:

type User @withSubscription
    ...
}

That’s all it takes to make the GraphQL API subscription-ready in Dgraph. Re-deploy your backend so that the changes can take effect. Now your endpoint can process subscription queries and send back appropriate responses to the client.

Enabling subscription on the client side using Apollo

We’re using Apollo as the GraphQL client of choice. Since it’s in charge of sending queries to the server, you need to configure it so that it can handle subscription queries too. Apollo has tools that you can use for that.

It all starts with installing the following package:

npm install subscriptions-transport-ws

In the previous article, we wrote two separate “links” for taking care of authentication headers and HTTP connection. Then we chained them together. With this library installed, we can do the same thing for handling WebSocket connection with the server.

In your src/Components/ApolloWrapper.js file, import the following definitions:

import { WebSocketLink } from "@apollo/client/link/ws";
import { getMainDefinition } from "@apollo/client/utilities";
import { split } from '@apollo/client';
Now write a new WebSocketLink in the following way:
const wsLink = new WebSocketLink({
  uri: process.env.REACT_APP_BACKEND_ENDPOINT.replace("https://", "wss://"),
  options: {
    reconnect: true,
    minTimeout: 30000,
    connectionParams: {
      "X-Auth-Token": xAuthToken.__raw,
    },
  },
});

What’s happening here?

  • We create a new “link” by using the WebSocketLink constructor.
    • The constructor takes the WebSocket URL as the uri parameter. Notice the protocol wss, which is the protocol for WebSocket URL.
    • The constructor also receives an options object containing any configuration we might choose to enforce.
      • We’re saying that we wish to automatically reconnect in case of any network errors.
      • We’re specifying a minimum time for establishing the connection between the client and the server. The default is 1000 ms. Disconnections could occur if Apollo reaches this threshold even before establishing the connection. So to be on the safe side, we specify a higher barrier.
      • Just like HTTP, we also want Dgraph to enforce the authorization rules we wrote earlier for WebSocket connections too. So we’re passing in the authentication header via connectionParams.

At this point, we have three links handling three different things. For GraphQL operations other than subscriptions, we should be using HTTP. That’s what we’ve been doing until now. When we create our client instance, we concatenate our httpLink with the authLink, for authenticated HTTP requests to the server.

For subscriptions, we need to tell Apollo to use WebSockets instead. We’ve already configured wsLink so that it has the authentication header.

So how do we proceed?

Apollo offers a tool split that we can use in this situation. It allows us to use specific “links” based on conditions.

In your ApolloWrapper.js file, put the following bit of code:

const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === "OperationDefinition" &&
      definition.operation === "subscription"
    );
  },
  wsLink,
  authLink.concat(httpLink)
);

What’s happening here?

  • The split function takes three arguments:
    • The first argument is a function. We can write our logic here and check what type of GraphQL operation is taking place. Using the getMainDefinition function, we’re checking whether the kind of operation is subscriptions or not.
    • The second argument is the “link” Apollo will use if the function argument returns a “truthy” value.
      • The return body of the function contains two boolean expressions connected by the logical AND (&&) operator. The function will return a “truthy” value only and only if both of the expressions evaluate to true. A “truthy” value means the operation kind is subscription.
    • If the function returns a “falsy” value, Apollo will use the “link” we supplied in the third argument.

What’s left is to just use splitLink that we just defined above as the link parameter in the ApolloClient constructor:

const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: splitLink,
});

After making all these changes, your ApolloWrapper.js file should look like this:

import {
 ApolloClient,
 ApolloProvider,
 InMemoryCache,
 createHttpLink,
 split,
} from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import { useAuth0 } from "@auth0/auth0-react";
import { useState, useEffect } from "react";
import { WebSocketLink } from '@apollo/client/link/ws';
import { getMainDefinition } from "@apollo/client/utilities";
import App from "../App";

const ApolloWrapper = () => {
 const { isAuthenticated, getIdTokenClaims } = useAuth0();
 const [xAuthToken, setXAuthToken] = useState("");

 useEffect(() => {
   const getToken = async () => {
     const token = isAuthenticated ? await getIdTokenClaims() : "";
     setXAuthToken(token);
   };
   getToken();
 }, [getIdTokenClaims, isAuthenticated]);

 const httpLink = createHttpLink({
   uri: process.env.REACT_APP_BACKEND_ENDPOINT,
 });

 const authLink = setContext((_, { headers, ...rest }) => {
   if (!xAuthToken) return { headers, ...rest };

   return {
     ...rest,
     headers: {
       ...headers,
       "X-Auth-Token": xAuthToken.__raw,
     },
   };
 });

 const wsLink = new WebSocketLink({
   uri: process.env.REACT_APP_BACKEND_ENDPOINT.replace("https://", "wss://"),
   options: {
     reconnect: true,
     minTimeout: 30000,
     connectionParams: {
       "X-Auth-Token": xAuthToken.__raw,
     },
   },
 });

 const splitLink = split(
   ({ query }) => {
     const definition = getMainDefinition(query);
     return (
       definition.kind === "OperationDefinition" &&
       definition.operation === "subscription"
     );
   },
   wsLink,
   authLink.concat(httpLink)
 );

 const client = new ApolloClient({
   cache: new InMemoryCache(),
   link: splitLink,
 });

 return (
   <ApolloProvider client={client}>
     <App />
   </ApolloProvider>
 );
};

export default ApolloWrapper;

Next thing to do on the client side is to change the query we had before to a subscription query.

Open your src/GraphQL/Queries/Queries.js file and just replace the query keyword with the subscription keyword:

export const QUERY_LOGGED_IN_USER = gql`
 subscription getUserInfo($userFilter: UserFilter!) {
    queryUser(filter: $userFilter) {
      email
      following {
        username
        avatarImageURL
        posts {
          id
          imageURL
          description
          likes
          postedBy {
            username
            avatarImageURL
          }
        }
      }
    }
  }
`;

Since this is a subscription query, we can’t use the useQuery hook for it. We’ll need to use the useSubscription hook instead. This works the same way as useQuery and returns three properties: data, loading, and error.

So in your src/Components/Feed.js file, import the useQuery hook:

import { useSubscription } from "@apollo/client";

And place it where you previously called the useQuery hook:

const { data, loading, error } = useSubscription(QUERY_LOGGED_IN_USER, {
  variables: {
    userFilter: {
      email: {
        eq: email,
      },
    },
  },
});

Now run npm start from your terminal. Your app should render like before.

The only difference is that now if any piece of data related to the subscription query changes on your backend, it’ll push those updates to the React app. As a result, the UI will update with the newest information in real-time, without the need to refresh your browser or restart the app. Let’s see that in action.

Testing it all out

Time to test out the subscription feature we just added. We’ll just change some data on the server from the Dgraph Cloud dashboard and see if the app reflects those changes.

In order to do that, you’ll need to log into felix’s account from the app, get the id_token, and use it as the X-Auth-Token header in the request. So follow the steps below:

  • Go to the root of your application source code and run npm start from the terminal.
  • Open up your browser’s development console and go to the Network tab.
  • You need to log in using the email address of the user felix. So log out if you’re logged in from another email.
  • After the UI renders, click on the Log in button. Log in using the email address you used to create the account of felix.
  • After a successful login, you should see a networking event with a file type named token in the Network tab.

Finding the token network response from browser&rsquo;s Network tab

  • Clicking on it will open its details. From there go to the Response tab.

Accessing id_token from the network response tab

  • Copy the id_token field.
  • Now go to the GraphQL window from the left sidebar of your Dgraph Cloud dashboard. Click on Request Headers above Explorer.
  • Paste your id_token as the value of X-Auth-Token header.

Go back to your app and log back in to the other account that was following felix. You should see posts from felix rendered again.

We’re ready to alter some data.

First we need to know the ids of felix’s posts. The update mutation will require them to filter out which posts need updating.

So fire the following query from GraphQL window first:

query {
  queryPost {
    id
    description
  }
}

Now execute the following mutation that changes the description of one of felix’s posts:

mutation UpdateDescription($updateData: UpdatePostInput!) {
  updatePost(input: $updateData) {
    post {
      id
      description
      likes
    }
  }
}

With the following value for $updateData variable:

{
  "updateData": {
    "filter": {
      "id": "0x60563948"
    },
    "set": {
      "description": "The beach was beautiful!"
    }
  }
}

You should see your app rendering the latest data as soon as you execute the mutation:

Conclusion

Today, we learned about GraphQL subscriptions. Subscriptions enable real-time updates for applications that need to serve the latest data to its users. Unlike normal queries, servers respond to subscription queries when the related data changes. So a lightweight persistent connection is established between the server and the client using WebSockets.

Enabling subscriptions in Dgraph is really simple. Continuing our exploration of different concepts throughout the Instagram clone series, today we played around with subscriptions by incorporating it into our app. We learned how to set up the client side using tools from the Apollo library. We kept various concerns separate by using “Apollo links”. We also learned how to establish authenticated WebSocket connections.

Stay in the loop as we explore more stuff in upcoming content!

Below are some relevant resources that you might find useful: