How to Use WebSockets in a GraphQL Application

2024-08-17

If you're building a modern GraphQL application that needs real-time features like chat functionality or live dashboard updates, you'll likely need to work with WebSockets. While GraphQL's request-response model works great for most operations, real-time updates require a different approach. The good news is that GraphQL has built-in support for subscriptions, which work beautifully with WebSockets.

I'll show you how to integrate WebSockets with GraphQL, focusing on the client side using Apollo Client, though the concepts apply to other GraphQL clients as well. You can find a complete working example in my minimal chat application repository that implements these concepts using Apollo Client and TypeScript.

Prerequisites

  • Understanding of GraphQL basics
  • Familiarity with Apollo Client
  • Basic knowledge of WebSocket concepts

Goals

  • Set up a WebSocket connection for GraphQL subscriptions
  • Implement real-time data updates using GraphQL subscriptions
  • Handle connection lifecycle and errors properly

Understanding GraphQL Subscriptions

Before diving into WebSockets, let's understand why we need them in GraphQL. Traditional GraphQL queries and mutations use HTTP requests, which are great for one-time data fetches or updates. However, for real-time updates, you'd need to repeatedly poll the server, which is inefficient. This is where subscriptions come in.

Subscriptions create a persistent connection between the client and server using WebSockets, allowing the server to push data to the client whenever relevant events occur. Here's a basic subscription example:

subscription OnNewMessage {
  messageCreated {
    id
    content
    sender
    timestamp
  }
}

First, let's create a utility class to manage our WebSocket connection. This abstraction will help us handle the connection lifecycle and provide a clean interface for our GraphQL client.

import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { createClient } from "graphql-ws";
 
class SubscriptionClient {
  constructor() {
    this.client = null;
  }
 
  connect(url, options = {}) {
    if (!this.client) {
      this.client = createClient({
        url,
        connectionParams: options.connectionParams,
        shouldRetry: true,
        retryAttempts: 3,
        ...options
      });
    }
    return new GraphQLWsLink(this.client);
  }
 
  disconnect() {
    if (this.client) {
      this.client.dispose();
      this.client = null;
    }
  }
}
 
export { SubscriptionClient };

This class provides a cleaner interface for managing WebSocket connections and can be extended with additional features like connection status monitoring or automatic reconnection logic.

Integrating with Apollo Client

Now let's set up Apollo Client to use our WebSocket link alongside the regular HTTP link. We'll use split to route subscription operations through the WebSocket connection while keeping queries and mutations on HTTP.

import { ApolloClient, InMemoryCache, split, HttpLink } from "@apollo/client";
import { getMainDefinition } from "@apollo/client/utilities";
import { SubscriptionClient } from "./SubscriptionClient";
 
const httpLink = new HttpLink({
  uri: "http://your-graphql-server/graphql"
});
 
const subscriptionClient = new SubscriptionClient();
const wsLink = subscriptionClient.connect("ws://your-graphql-server/graphql");
 
const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === "OperationDefinition" &&
      definition.operation === "subscription"
    );
  },
  wsLink,
  httpLink
);
 
const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache()
});

Using Subscriptions in Components

Here's how you can use subscriptions in a React component to receive real-time updates:

import { useSubscription } from "@apollo/client";
import { gql } from "graphql-tag";
 
const NEW_MESSAGE_SUBSCRIPTION = gql`
  subscription OnNewMessage {
    messageCreated {
      id
      content
      sender
      timestamp
    }
  }
`;
 
function ChatRoom() {
  const { data, loading, error } = useSubscription(NEW_MESSAGE_SUBSCRIPTION, {
    onData: ({ data }) => {
      // Handle incoming message
      console.log("New message received:", data.messageCreated);
    }
  });
 
  if (loading) return <p>Listening for messages...</p>;
  if (error) return <p>Error :( {error.message}</p>;
 
  return (
    <div>
      <h2>Chat Room</h2>
      {/* Your chat UI */}
    </div>
  );
}

Handling Connection Lifecycle

It's important to properly manage the WebSocket connection lifecycle, especially in single-page applications where components might mount and unmount frequently:

import { useEffect } from "react";
import { useApolloClient } from "@apollo/client";
 
function ChatApp() {
  const client = useApolloClient();
 
  useEffect(() => {
    // Connection is handled automatically when using the subscription
 
    return () => {
      // Clean up subscription on component unmount
      client.clearStore();
    };
  }, []);
 
  return <ChatRoom />;
}

Best Practices

  1. Error Handling: Always implement proper error handling for your subscriptions. WebSocket connections can fail for various reasons, and your application should gracefully handle these situations.

  2. Connection Management: Consider implementing reconnection logic with exponential backoff for production applications.

  3. Authentication: If your application requires authentication, pass the auth tokens through the connection parameters:

const wsLink = subscriptionClient.connect(WS_URL, {
  connectionParams: {
    authToken: user.token
  }
});
  1. Performance: Be mindful of the number of active subscriptions. Each subscription maintains an open connection, so unsubscribe when the data is no longer needed.

Conclusion

WebSockets and GraphQL subscriptions provide a powerful combination for building real-time features in your applications. By properly structuring your WebSocket implementation and following best practices, you can create robust real-time applications that scale well.

Remember that each browser tab will create its own WebSocket connection by default. For applications that need to optimize connection usage across multiple tabs, consider using the SharedWorker API and BroadcastChannel API - a topic I'll cover in a future article.

The approach outlined here provides a solid foundation for implementing real-time features in your GraphQL applications, but you can extend it further based on your specific needs, such as adding reconnection strategies, implementing presence detection, or handling complex authentication scenarios.