Building Real-Time Applications with GraphQL Subscriptions and React

Have you ever wanted to build a web application that can update in real-time, without the user having to refresh the page? With GraphQL subscriptions and React, you can do just that! In this tutorial, we'll show you how to set up GraphQL subscriptions on the server-side and how to use them with the Apollo Client in your React app. By the end of this article, you'll be able to build real-time web applications with ease.

What are GraphQL Subscriptions?

GraphQL is a query language for APIs, and it allows you to build APIs in a way that's flexible and powerful. One of the features that GraphQL provides is subscriptions. Subscriptions are a way to receive real-time updates from an API whenever data changes. With subscriptions, you can build web applications that update in real-time, just like a chat application or a stock ticker.

GraphQL subscriptions work by creating a long-lived connection between the client and server. Whenever data changes on the server, it sends a message to all connected clients, and the clients can update their UI accordingly. This is different from a traditional REST API, where the client has to make repeated requests to get the latest data.

Now that we have a basic understanding of GraphQL subscriptions, let's dive into building a real-time web application with React and GraphQL subscriptions.

Setting up GraphQL Subscriptions on the Server Side

Before we can use GraphQL subscriptions in our React app, we need to set them up on the server side. In this tutorial, we'll be using Apollo Server and MongoDB.

First, let's install the necessary dependencies. Open up your terminal and run the following command:

$ npm install apollo-server graphql mongoose

Next, create a new file called index.js and add the following code:

// index.js
const { ApolloServer, PubSub } = require('apollo-server');
const mongoose = require('mongoose');
const typeDefs = require('./typeDefs');
const resolvers = require('./resolvers');

mongoose.connect('mongodb://localhost:27017/myapp', {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  useFindAndModify: false,
  useCreateIndex: true
}).then(() => {
  console.log('MongoDB connected');
}).catch((error) => {
  console.log(error);
});

const pubsub = new PubSub();

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => ({ req, pubsub })
});

server.listen().then(({ url }) => {
  console.log(`Server listening on ${url}`);
});
  

Let's go through this code step by step. First, we import the necessary dependencies: ApolloServer, PubSub, mongoose, and our own typeDefs and resolvers.

Next, we connect to our MongoDB database using mongoose.connect. Make sure to replace myapp with your own database name.

We then create a new instance of PubSub, which is used to publish and subscribe to messages.

After that, we create a new instance of ApolloServer and pass in our typeDefs, resolvers, and a context object. The context object is where we can pass in any values that we want to make available to our resolvers. In this case, we're passing in req (the current request object) and pubsub (our instance of PubSub).

Finally, we start the server and log the URL that it's running on. Our server is now ready to handle GraphQL queries and subscriptions!

Creating the GraphQL Schema

Now that we have our server set up, let's create our GraphQL schema. In this tutorial, we'll be building a simple chat application. We'll have a Message type that represents a message, and a Query, Mutation, and Subscription that allow us to retrieve, create, and subscribe to messages, respectively.

Create a new folder called schema, and inside that folder, create a new file called message.js. Add the following code:

// schema/message.js
const { gql } = require('apollo-server');

const typeDefs = gql`
  type Message {
    id: ID!
    text: String!
  }

  type Query {
    messages: [Message]!
  }

  type Mutation {
    addMessage(text: String!): Message!
  }

  type Subscription {
    newMessage: Message!
  }
`;

module.exports = typeDefs;
  

Let's go through this code step by step. First, we import gql from apollo-server.

We then define a Message type with two fields: id (an ID) and text (a string).

Next, we define a Query type with one field: messages (an array of Message objects).

We then define a Mutation type with one field: addMessage (which takes a text argument and returns a Message object).

Finally, we define a Subscription type with one field: newMessage (which returns a Message object).

We export our schema using module.exports. That's it for our schema!

Creating the Resolvers

Next, let's create our resolvers. Resolvers are functions that handle GraphQL queries, mutations, and subscriptions.

Create a new folder called resolvers, and inside that folder, create a new file called message.js. Add the following code:

// resolvers/message.js
const Message = require('../models/Message');
const { PubSub } = require('apollo-server');

const pubsub = new PubSub();

const MESSAGE_ADDED = 'MESSAGE_ADDED';

const resolvers = {
  Query: {
    messages: async () => {
      const messages = await Message.find().sort({ createdAt: -1 });
      return messages;
    }
  },
  Mutation: {
    addMessage: async (_, { text }) => {
      const newMessage = new Message({
        text
      });
      const message = await newMessage.save();
      pubsub.publish(MESSAGE_ADDED, { newMessage: message });
      return message;
    }
  },
  Subscription: {
    newMessage: {
      subscribe: () => pubsub.asyncIterator([MESSAGE_ADDED])
    }
  }
};

module.exports = resolvers;
  

Let's go through this code step by step. First, we import our Message model and PubSub from apollo-server.

We then create a new instance of PubSub. This is the same instance that we passed to our Apollo Server earlier.

Next, we define a constant called MESSAGE_ADDED that we'll use as the topic for our subscription.

We then define our resolvers. First, we have a Query resolver for messages, which returns an array of messages sorted by their createdAt date in descending order.

Next, we have a Mutation resolver for addMessage, which takes a text argument and creates a new message in our MongoDB database. We then publish a message to our MESSAGE_ADDED topic using pubsub.publish, and return the newly created message.

Finally, we have a Subscription resolver for newMessage, which uses pubsub.asyncIterator to subscribe to our MESSAGE_ADDED topic and return any new messages that are published. This resolver has to return an async iterator, which is what allows it to receive real-time updates.

We export our resolvers using module.exports. That's it for our resolvers!

Connecting to the GraphQL API from a React Application

Now that our GraphQL API is set up, let's connect to it from our React application. In this tutorial, we'll be using the Apollo Client to do this.

First, let's install the necessary dependencies. Open up your terminal and run the following command:

$ npm install apollo-boost @apollo/react-hooks subscriptions-transport-ws

apollo-boost is a package that provides a pre-configured Apollo Client, @apollo/react-hooks is a package that provides hooks to use with the Apollo Client, and subscriptions-transport-ws is a package that provides WebSocket support for subscriptions.

Next, create a new file called index.js in your React app's src folder, and add the following code:

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloProvider } from '@apollo/react-hooks';
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { WebSocketLink } from 'apollo-link-ws';
import { split } from 'apollo-link';
import { getMainDefinition } from 'apollo-utilities';
import App from './App';

const httpLink = new HttpLink({
  uri: 'http://localhost:4000/graphql'
});

const wsLink = new WebSocketLink({
  uri: 'ws://localhost:4000/graphql',
  options: {
    reconnect: true
  }
});

const link = split(({ query }) => {
  const definition = getMainDefinition(query);
  return (
    definition.kind === 'OperationDefinition' &&
    definition.operation === 'subscription'
  );
}, wsLink, httpLink);

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

ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>,
  document.getElementById('root')
);
  

Let's go through this code step by step. First, we import the necessary dependencies. We're using the WebSocketLink from apollo-link-ws to handle WebSocket subscriptions, and we're using the split function from apollo-link and the getMainDefinition function from apollo-utilities to split our link and send subscriptions over the WebSocketLink and everything else over the HttpLink.

Next, we create a new instance of HttpLink with the URL of our GraphQL API.

We then create a new instance of WebSocketLink with the WebSocket URL of our GraphQL API, and set the reconnect option to true so that the client automatically reconnects if it loses the WebSocket connection.

Next, we create a new link using split that decides whether to use the WebSocketLink or HttpLink based on the type of operation being performed. If it's a subscription, we use the WebSocketLink; if it's anything else, we use the HttpLink.

Finally, we create a new instance of ApolloClient with our link and an InMemoryCache, and wrap our App component in an ApolloProvider that passes the client down to all child components.

Our React application is now connected to our GraphQL API using the Apollo Client!

Using GraphQL Subscriptions in React

Now that we have our React app connected to our GraphQL API, let's use GraphQL subscriptions to receive real-time updates whenever a new message is added.

Create a new file called Chat.js in your React app's src folder, and add the following code:

// src/Chat.js
import React, { useState } from 'react';
import { useSubscription, useMutation } from '@apollo/react-hooks';
import { gql } from 'apollo-boost';

const MESSAGES_QUERY = gql`
  {
    messages {
      id
      text
    }
  }
`;

const MESSAGE_ADDED_SUBSCRIPTION = gql`
  subscription {
    newMessage {
      id
      text
    }
  }
`;

const ADD_MESSAGE_MUTATION = gql`
  mutation AddMessage($text: String!) {
    addMessage(text: $text) {
      id
      text
    }
  }
`;

function Chat() {
  const [text, setText] = useState('');
  const [addMessage] = useMutation(ADD_MESSAGE_MUTATION);

  const sendChatMessage = () => {
    addMessage({
      variables: {
        text: text
      },
      optimisticResponse: {
        addMessage: {
          __typename: 'Message',
          id: -1,
          text: text
        }
      },
      update: (proxy, { data: { addMessage } }) => {
        const data = proxy.readQuery({ query: MESSAGES_QUERY });
        data.messages.unshift(addMessage);
        proxy.writeQuery({ query: MESSAGES_QUERY, data });
      }
    });

    setText('');
  };

  const { loading, error, data } = useSubscription(
    MESSAGE_ADDED_SUBSCRIPTION
  );

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <div>
      <ul>
        {data.messages.map(message => (
          <li key={message.id}>{message.text}</li>
        ))}
      </ul>
      <div>
        <input type="text" value={text} onChange={e => setText(e.target.value)} />
        <button onClick={sendChatMessage}>Send</button>
      </div>
    </div>
  );
}

export default Chat;
  

Let's go through this code step by step. First, we define our MESSAGES_QUERY, which retrieves an array of messages, and our MESSAGE_ADDED_SUBSCRIPTION, which subscribes to new messages.

We then define our ADD_MESSAGE_MUTATION, which takes a text argument and creates a new message in our MongoDB database. We're also using optimisticResponse to optimistically update the UI before the server responds, and update to update the cache with the newly created message.