I recently made a super simple To Do List that uses GraphQL subscriptions provided by Apollo to update when my "database" (a JavaScript object) is updated.
Our objective is to create a To Do with a title: String and an id: ID. When we create a new To Do on our frontend, we want it to receive a subscription notification and for our frontend to be updated.
I stripped out as much fat as possible so that I could focus on how GraphQL subscriptions can be implemented on the frontend.
You can find the full repo here https://github.com/jackbridger/MinimalGQLSubscriptions and I'll talk through some of the key aspects below.
Let's first take a look at our server file and particularly the parts that relate to subscriptions.
To notify our frontend when our database has been updated, we use PubSub, which is based on an event emitter.
import { GraphQLServer, PubSub } from "graphql-yoga"
const pubsub = new PubSub();
pubsub provides us with two useful functions:
- publish
- asyncIterator
We access these by passing pubsub into the context of our resolvers.
When we create a To Do we want two things to happen on our server.
- Emit an event on the TODOS_CHANGED channel with the created ToDo as the payload.
- A subscription operation that is listening for TODOS_CHANGED forward our ToDoChanged payload to all clients that have an open web socket listening for ToDoChanged susbcription
const resolvers = {
Query: {
// Return all To Dos
toDos: (_, __, { db }) => {
return db.toDos;
}
},
Mutation: {
createToDo: (_, { title }) => {
const id = createRandomId();
const newToDo = { id, title };
db.toDos.push(newToDo);
// Notify susbscriptions listening to the TODOS_CHANGED channel
// That a to do has changed and sending through that the newToDo as
// the ToDoChanged payload
pubsub.publish(TODOS_CHANGED, { ToDoChanged: newToDo });
return newToDo;
}
},
Subscription: {
// Note: "Subscriptions resolvers are not a function,
// but an object with subscribe method, that returns AsyncIterable."
ToDoChanged: {
subscribe(_, __, { pubsub }) {
// Listen for TODOS_CHANGED changed and then forward the provided
// ToDoChanged payload to clients who have subscribed to ToDoChanged
return pubsub.asyncIterator(TODOS_CHANGED);
}
}
}
}
Note: "Subscriptions resolvers are not a function, but an object with subscribe method, that returns AsyncIterable."
On our front end let's start in index.js - practically the whole file is relevant and I've annotated what is happening.
Essentially we are creating two path ways - a http pathway for our queries and our mutations and a web socket pathway for our subscriptions. Terminating link directs each operation to the right link.
import { ApolloClient } from 'apollo-client';
import { ApolloProvider } from "@apollo/react-hooks"
import { InMemoryCache } from 'apollo-cache-inmemory';
import { split } from 'apollo-link';
import { WebSocketLink } from 'apollo-link-ws';
import { HttpLink } from 'apollo-link-http';
// The http link is a terminating link that fetches GraphQL results from a GraphQL
// endpoint over an http connection
const httpLink = new HttpLink({
uri: 'http://localhost:4000/'
});
// Allow you to send/receive subscriptions over a web socket
const wsLink = new WebSocketLink({
uri: 'ws://localhost:4000/',
options: {
reconnect: true
}
});
// Acts as "middleware" for directing our operations over http or via web sockets
const terminatingLink = split(
({ query: { definitions } }) =>
definitions.some(node => {
const { kind, operation } = node;
return kind === 'OperationDefinition' && operation === 'subscription';
}),
wsLink,
httpLink
);
// Create a new client to make requests with, use the appropriate link returned
// by termintating link (either ws or http)
const client = new ApolloClient({
cache: new InMemoryCache(),
link: terminatingLink
});
ReactDOM.render(<ApolloProvider client={client}>
<App />
</ApolloProvider>
, document.getElementById('root'));
We wrap our app in ApolloProvider and pass in the client that allows us to interact with the GraphQL server.
In App.js, when the page loads we query for all To Dos:
const {
subscribeToMore, // subscribe to new to dos
data, // To do data
loading, // true or false if the data is currently loading
error // null or error object if failed to fetch
} = useQuery(TODO_QUERY)
This not only allows us to display the existing To Dos, it provides us with a function that allows us to subscribe to more To Dos.
We only want to call this when our To Dos component mounts so we wrap it inside a function expression and call it when our ToDoList component mounts.
//App.js
const subscribeToNewToDos = () =>
subscribeToMore({
document: TODO_SUBSCRIPTION, // the gql subscription operation
// How do we update our ToDos data when subscription data comes through.
updateQuery: (currentToDos, { subscriptionData }) => {
if (!subscriptionData.data) return currentToDos;
const newToDo = subscriptionData.data.ToDoChanged;
const updatedToDos = currentToDos.toDos.concat(newToDo)
setToDos(updatedToDos) // Update the state of todos with new to do
return { toDos: updatedToDos } // return the todos in the format expected
}
})
//ToDoList.js
//We pass in subscribeToNewToDos to ToDoList component as a prop and call it when the component is mounted
React.useEffect(() => subscribeToNewToDos(), []);
setToDos updates the state of toDos every time a subscription notification is received.
We return the new state of ToDos - if we don't do that then the most recent To Do will be overwritten every time a new To Do comes in.
I've skipped out much of the implementation in React but you can see it in the repo.
Please let me know in the comments if you have any suggestions on how to do this more effectively or if you would clarify any of the explanations.
If you like this article, I also host a podcast on developer tools.
Oldest comments (0)