Subscriptions
Learn how to achieve realtime data with GraphQL subscriptions
In addition to fetching data using queries and modifying data using mutations, the GraphQL spec supports a third operation type, called subscription
.
GraphQL subscriptions are a way to push data from the server to the clients that choose to listen to real time messages from the server. Subscriptions are similar to queries in that they specify a set of fields to be delivered to the client, but instead of immediately returning a single answer, a result is sent every time a particular event happens on the server.
A common use case for subscriptions is notifying the client side about particular events, for example the creation of a new object, updated fields and so on.
This is an advanced feature that Apollo Boost does not support. Learn how to set Apollo Client up manually in our Apollo Boost migration guide.
Overview
GraphQL subscriptions have to be defined in the schema, just like queries and mutations:
1type Subscription {
2 commentAdded(repoFullName: String!): Comment
3}
On the client, subscription queries look just like any other kind of operation:
1subscription onCommentAdded($repoFullName: String!){
2 commentAdded(repoFullName: $repoFullName){
3 id
4 content
5 }
6}
The response sent to the client looks as follows:
1{
2 "data": {
3 "commentAdded": {
4 "id": "123",
5 "content": "Hello!"
6 }
7 }
8}
In the above example, the server is written to send a new result every time a comment is added on GitHunt for a specific repository. Note that the code above only defines the GraphQL subscription in the schema. Read setting up subscriptions on the client and setting up GraphQL subscriptions for the server to learn how to add subscriptions to your app.
When to use subscriptions
In most cases, intermittent polling or manual refetching are actually the best way to keep your client up to date. So when is a subscription the best option? Subscriptions are especially useful if:
The initial state is large, but the incremental change sets are small. The starting state can be fetched with a query and subsequently updated through a subscription.
You care about low-latency updates in the case of specific events, for example in the case of a chat application where users expect to receive new messages in a matter of seconds.
A future version of Apollo or GraphQL might include support for live queries, which would be a low-latency way to replace polling, but at this point general live queries in GraphQL are not yet possible outside of some relatively experimental setups.
Client setup
The most popular transport for GraphQL subscriptions today is subscriptions-transport-ws
. This package is maintained by the Apollo community, but can be used with any client or server GraphQL implementation. In this article, we'll explain how to set it up on the client, but you'll also need a server implementation. You can read about how to use subscriptions with a JavaScript server, or enjoy subscriptions set up out of the box if you are using a GraphQL backend as a service like Graphcool or Scaphold.
Let's look at how to add support for this transport to Apollo Client.
First, install the WebSocket Apollo Link (apollo-link-ws
) from npm:
1npm install --save apollo-link-ws subscriptions-transport-ws
Then, initialize a GraphQL subscriptions transport link:
1import { WebSocketLink } from 'apollo-link-ws';
2
3const wsLink = new WebSocketLink({
4 uri: `ws://localhost:5000/`,
5 options: {
6 reconnect: true
7 }
8});
1import { split } from 'apollo-link';
2import { HttpLink } from 'apollo-link-http';
3import { WebSocketLink } from 'apollo-link-ws';
4import { getMainDefinition } from 'apollo-utilities';
5
6// Create an http link:
7const httpLink = new HttpLink({
8 uri: 'http://localhost:3000/graphql'
9});
10
11// Create a WebSocket link:
12const wsLink = new WebSocketLink({
13 uri: `ws://localhost:5000/`,
14 options: {
15 reconnect: true
16 }
17});
18
19// using the ability to split links, you can send data to each link
20// depending on what kind of operation is being sent
21const link = split(
22 // split based on operation type
23 ({ query }) => {
24 const definition = getMainDefinition(query);
25 return (
26 definition.kind === 'OperationDefinition' &&
27 definition.operation === 'subscription'
28 );
29 },
30 wsLink,
31 httpLink,
32);
Now, queries and mutations will go over HTTP as normal, but subscriptions will be done over the websocket transport.
useSubscription Hook
The easiest way to bring live data to your UI is by using React Apollo's useSubscription
Hook. This lets you render the stream of data from your service directly within your render function of your component! One thing to note, subscriptions are just listeners, they don't request any data when first connected, but only open up a connection to get new data. Binding live data to your UI is as easy as this:
1const COMMENTS_SUBSCRIPTION = gql`
2 subscription onCommentAdded($repoFullName: String!) {
3 commentAdded(repoFullName: $repoFullName) {
4 id
5 content
6 }
7 }
8`;
9
10function DontReadTheComments({ repoFullName }) {
11 const { data: { commentAdded }, loading } = useSubscription(
12 COMMENTS_SUBSCRIPTION,
13 { variables: { repoFullName } }
14 );
15 return <h4>New comment: {!loading && commentAdded.content}</h4>;
16}
1const COMMENTS_SUBSCRIPTION = gql`
2 subscription onCommentAdded($repoFullName: String!) {
3 commentAdded(repoFullName: $repoFullName) {
4 id
5 content
6 }
7 }
8`;
9
10const DontReadTheComments = ({ repoFullName }) => (
11 <Subscription
12 subscription={COMMENTS_SUBSCRIPTION}
13 variables={{ repoFullName }}
14 >
15 {({ data: { commentAdded }, loading }) => (
16 <h4>New comment: {!loading && commentAdded.content}</h4>
17 )}
18 </Subscription>
19);
useSubscription API overview
If you're looking for an overview of all the options useSubscription
accepts, and its result properties, look no further!
Note: If you're using React Apollo's
Subscription
render prop component, the option/result details listed below are still valid (options are component props and results are passed into the render prop function). The only difference is that asubscription
prop (which holds a GraphQL subscription document parsed into an AST bygraphql-tag
) is also required.
Options
The useSubscription
Hook accepts the following options.
Option | Type | Description |
---|---|---|
subscription | DocumentNode | A GraphQL subscription document parsed into an AST by graphql-tag . Optional for the useSubscription Hook since the subscription can be passed in as the first parameter to the Hook. Required for the Subscription component. |
variables | { [key: string]: any } | An object containing all of the variables your subscription needs to execute |
shouldResubscribe | boolean | Determines if your subscription should be unsubscribed and subscribed again |
skip | boolean | If skip is true , the subscription will be skipped entirely |
onSubscriptionData | (options: OnSubscriptionDataOptions<TData>) => any | Allows the registration of a callback function, that will be triggered each time the useSubscription Hook / Subscription component receives data. The callback options object param consists of the current Apollo Client instance in client , and the received subscription data in subscriptionData . |
fetchPolicy | FetchPolicy | How you want your component to interact with the Apollo cache. Defaults to "cache-first". |
client | ApolloClient | An ApolloClient instance. By default useSubscription / Subscription uses the client passed down via context, but a different client can be passed in. |
Result
After being called the useSubscription
Hook will return a result object with the following properties.
Property | Type | Description |
---|---|---|
data | TData | An object containing the result of your GraphQL subscription. Defaults to an empty object. |
loading | boolean | A boolean that indicates whether any initial data has been returned |
error | ApolloError | A runtime error with graphQLErrors and networkError properties |
subscribeToMore
With GraphQL subscriptions your client will be alerted on push from the server and you should choose the pattern that fits your application the most:
Use it as a notification and run any logic you want when it fires, for example alerting the user or refetching data
Use the data sent along with the notification and merge it directly into the store (existing queries are automatically notified)
With subscribeToMore
, you can easily do the latter.
subscribeToMore
is a function available on every query result in React Apollo. It works just like [fetchMore
caching/cache-interaction/#incremental-loading-fetchmore), except that the update function gets called every time the subscription returns, instead of only once.
Here is a regular query:
1const COMMENT_QUERY = gql`
2 query Comment($repoName: String!) {
3 entry(repoFullName: $repoName) {
4 comments {
5 id
6 content
7 }
8 }
9 }
10`;
11
12function CommentsPageWithData({ params }) {
13 const result = useQuery(
14 COMMENT_QUERY,
15 { variables: { repoName: `${params.org}/${params.repoName}` } }
16 );
17 return <CommentsPage {...result} />;
18}
1const COMMENT_QUERY = gql`
2 query Comment($repoName: String!) {
3 entry(repoFullName: $repoName) {
4 comments {
5 id
6 content
7 }
8 }
9 }
10`;
11
12const CommentsPageWithData = ({ params }) => (
13 <Query
14 query={COMMENT_QUERY}
15 variables={{ repoName: `${params.org}/${params.repoName}` }}
16 >
17 {result => <CommentsPage {...result} />}
18 </Query>
19);
Now, let's add the subscription.
Add a function called subscribeToNewComments
that will subscribe using subscribeToMore
and update the query's store with the new data using updateQuery
.
Note that the updateQuery
callback must return an object of the same shape as the initial query data, otherwise the new data won't be merged. Here the new comment is pushed in the comments
list of the entry
:
1const COMMENT_QUERY = gql`
2 query Comment($repoName: String!) {
3 entry(repoFullName: $repoName) {
4 comments {
5 id
6 content
7 }
8 }
9 }
10`;
11
12const COMMENTS_SUBSCRIPTION = gql`
13 subscription onCommentAdded($repoName: String!) {
14 commentAdded(repoName: $repoName) {
15 id
16 content
17 }
18 }
19`;
20
21function CommentsPageWithData({ params }) {
22 const { subscribeToMore, ...result } = useQuery(
23 COMMENT_QUERY,
24 { variables: { repoName: `${params.org}/${params.repoName}` } }
25 );
26
27 return (
28 <CommentsPage
29 {...result}
30 subscribeToNewComments={() =>
31 subscribeToMore({
32 document: COMMENTS_SUBSCRIPTION,
33 variables: { repoName: params.repoName },
34 updateQuery: (prev, { subscriptionData }) => {
35 if (!subscriptionData.data) return prev;
36 const newFeedItem = subscriptionData.data.commentAdded;
37
38 return Object.assign({}, prev, {
39 entry: {
40 comments: [newFeedItem, ...prev.entry.comments]
41 }
42 });
43 }
44 })
45 }
46 />
47 );
48}
1const COMMENT_QUERY = gql`
2 query Comment($repoName: String!) {
3 entry(repoFullName: $repoName) {
4 comments {
5 id
6 content
7 }
8 }
9 }
10`;
11
12const COMMENTS_SUBSCRIPTION = gql`
13 subscription onCommentAdded($repoName: String!) {
14 commentAdded(repoName: $repoName) {
15 id
16 content
17 }
18 }
19`;
20
21const CommentsPageWithData = ({ params }) => (
22 <Query
23 query={COMMENT_QUERY}
24 variables={{ repoName: `${params.org}/${params.repoName}` }}
25 >
26 {({ subscribeToMore, ...result }) => (
27 <CommentsPage
28 {...result}
29 subscribeToNewComments={() =>
30 subscribeToMore({
31 document: COMMENTS_SUBSCRIPTION,
32 variables: { repoName: params.repoName },
33 updateQuery: (prev, { subscriptionData }) => {
34 if (!subscriptionData.data) return prev;
35 const newFeedItem = subscriptionData.data.commentAdded;
36
37 return Object.assign({}, prev, {
38 entry: {
39 comments: [newFeedItem, ...prev.entry.comments]
40 }
41 });
42 }
43 })
44 }
45 />
46 )}
47 </Query>
48);
and start the actual subscription by calling the subscribeToNewComments
function with the subscription variables:
1export class CommentsPage extends Component {
2 componentDidMount() {
3 this.props.subscribeToNewComments();
4 }
5}
Authentication over WebSocket
In many cases it is necessary to authenticate clients before allowing them to receive subscription results. To do this, the SubscriptionClient
constructor accepts a connectionParams
field, which passes a custom object that the server can use to validate the connection before setting up any subscriptions.
1import { WebSocketLink } from 'apollo-link-ws';
2
3const wsLink = new WebSocketLink({
4 uri: `ws://localhost:5000/`,
5 options: {
6 reconnect: true,
7 connectionParams: {
8 authToken: user.authToken,
9 },
10});
You can use
connectionParams
for anything else you might need, not only authentication, and check its payload on the server side with SubscriptionsServer.