Pup

v2.x

GraphQLUsing Subscriptions

Once we have our subscriptions defined in our schema, the next step is to actually put them to use. Fortunately, the Apollo framework we use in Pup to interact with GraphQL from the client has a mechanism for consuming subscriptions with ease: subscribeToMore.

Reprising our commentAdded example from our schema, on the right, we've pulled in the Comments.gql file from the /ui/subscriptions folder in Pup along with the <ViewDocument /> component.

In the component, we can see the /ui/subscriptions/Comments.gql file being imported as commentAdded.

If we recall from our schema, comments are not queries on their own. Instead, we set up our schema to return comments as a nested field on documents. Here in the <ViewDocument /> component, we run the document query from the /ui/queries/Documents.gql file.

In the data returned from this query, we anticipate any comments that have been added to the document. As a convenience, because a subscription exists in relation to that data—thanks to our commentAdded subscription—we can utilize the .subscribeToMore() method that's attached to the data prop passed to our component when we load our query via the graphql() component enhancer at the bottom of our file.

What's neat about this function is that it can take a subscription query document as an argument and "subscribe" to that data. When new data is published to that subscription, the updateQuery method defined on the object passed to data.subscribeToMore() is invoked, passing the existing data in the client side cache—here, the document we're viewing—along with the subscriptionData or payload passed from our call to pubsub.publish() on the server.

Inside of that function, we can return a value representing the existing piece of data in the cache with some modifications. In this example, we want to return the entire document, however, we want to modify its comments array value to include both the new comment that was added (accessible via subscriptionData.data.commentAdded) and the existingData.document.comments value.

In doing this, we create a new array that contains both the existing comments and the new comment. This value, then, is returned from updateQuery to replace the current value in the client-side cache. The result? Apollo triggers a re-render on the client and the new comment appears on screen for our user! In other words: real-time data.

To make sure this is clear, too, if we look at the <Comments /> component below, we can see that the prop subscribeToNewComments where we call to data.subscribeToMore is passed in and called in the componentDidMount() event of the <Comments /> component (effectively establishing the subscription when the <Comments /> component renders on screen).

/ui/subscriptions/Comments.gql
subscription commentAdded($documentId: String!) {
  commentAdded(documentId: $documentId) {
    _id
    documentId
    comment
    createdAt
    user {
      _id
      name {
        first
        last
      }
    }
  }
}
/ui/pages/ViewDocument/index.js
import React from 'react';
import PropTypes from 'prop-types';
import { graphql } from 'react-apollo';
import { Meteor } from 'meteor/meteor';
import SEO from '../../components/SEO';
import BlankState from '../../components/BlankState';
import Comments from '../../components/Comments';
import { document as documentQuery } from '../../queries/Documents.gql';
import commentAdded from '../../subscriptions/Comments.gql';
import parseMarkdown from '../../../modules/parseMarkdown';

import { StyledViewDocument, DocumentBody } from './styles';

class ViewDocument extends React.Component {
  componentWillMount() {
    const { data } = this.props;
    if (Meteor.isClient && Meteor.userId()) data.refetch();
  }

  render() {
    const { data } = this.props;

    if (!data.loading && data.document) {
      return (
        <React.Fragment>
          <StyledViewDocument>
            <SEO
              title={data.document && data.document.title}
              description={data.document && data.document.body}
              url={`documents/${data.document && data.document._id}`}
              contentType="article"
              published={data.document && data.document.createdAt}
              updated={data.document && data.document.updatedAt}
              twitter="clvrbgl"
            />
            <React.Fragment>
              <h1>{data.document && data.document.title}</h1>
              <DocumentBody
                dangerouslySetInnerHTML={{
                  __html: parseMarkdown(data.document && data.document.body),
                }}
              />
            </React.Fragment>
          </StyledViewDocument>
          <Comments
            subscribeToNewComments={() =>
              data.subscribeToMore({
                document: commentAdded,
                variables: {
                  documentId: data.document && data.document._id,
                },
                updateQuery: (existingData, { subscriptionData }) => {
                  if (!subscriptionData.data) return existingData;
                  const newComment = subscriptionData.data.commentAdded;
                  return {
                    document: {
                      ...existingData.document,
                      comments: [...existingData.document.comments, newComment],
                    },
                  };
                },
              })
            }
            documentId={data.document && data.document._id}
            comments={data.document && data.document.comments}
          />
        </React.Fragment>
      );
    }

    if (!data.loading && !data.document) {
      return (
        <BlankState
          icon={{ style: 'solid', symbol: 'file-alt' }}
          title="No document here, friend!"
          subtitle="Make sure to double check the URL! If it's correct, this is probably a private document."
        />
      );
    }

    return null;
  }
}

ViewDocument.propTypes = {
  data: PropTypes.object.isRequired,
};

export default graphql(documentQuery, {
  options: ({ match }) => ({
    variables: {
      _id: match.params._id,
    },
  }),
})(ViewDocument);
/ui/components/Comments/index.js
import React from 'react';
import PropTypes from 'prop-types';
import CommentComposer from '../CommentComposer';
import { timeago } from '../../../modules/dates';

import { StyledComments, CommentsList, Comment } from './styles';

class Comments extends React.Component {
  componentDidMount() {
    this.props.subscribeToNewComments();
  }

  render() {
    const { documentId, comments } = this.props;
    return (
      <StyledComments>
        [...]
      </StyledComments>
    );
  }
}

Comments.propTypes = {
  documentId: PropTypes.string.isRequired,
  comments: PropTypes.array.isRequired,
  subscribeToNewComments: PropTypes.func.isRequired,
};

export default Comments;