So far we've been developing the app and we've been treating it as if there's no other users; we're the only one exists. This approach is true when we want to develop a UI and focus on UX, but comes a point where we need to start thinking on a macro level. Our app is social interactive, and if things work properly for me, it doesn't mean that it works properly to the fellow I'm chatting with. It's inevitable to have an authentication system in our app, hence we need to take care of things before we get to that stage.
Try to open 2 instances of the app in 2 separate tabs/windows, and navigate into the same chat room. Try to send a message with one instance and notice that the second instance doesn't update unless we refresh the page.
This issue is very important and should be addressed, because a chat is all about sending and receiving messages on a lively basis. This issue was expected, as there's no mechanism that would trigger and listen to changes in the back-end. In this chapter we're gonna address that issue by implementing exactly that mechanism.
Introducing: GraphQL Subscriptions
GraphQL subscriptions is a mechanism that works on web-sockets and live communication; clients can subscribe to it and be notified regards specific changes that happen in the back-end. Notifications will be triggered manually by us and can be provided with parameters that provide additional information regards the triggered event. For example, a messageAdded
will be published with the new message, and will notify all clients who are subscribed to that event. Once the subscribers are notified, they can respond as they would like to, such as updating the UI.
A subscription is presented in our GraphQL schema as a separate type called Subscription
, where each field represents an event name along with its return type.
Like any other GraphQL type, each field should be match with a resolver where we handle the request.
In this chapter we will implement the messageAdded
subscription, so users can be notified when it happens and update the messages list to contain the new message.
Implementing a subscription
We will start by creating a new Subscription
type in our GraphQL schema with the field messageAdded
:
@@ -23,3 +23,7 @@
┊23┊23┊type Mutation {
┊24┊24┊ addMessage(chatId: ID!, content: String!): Message
┊25┊25┊}
+┊ ┊26┊
+┊ ┊27┊type Subscription {
+┊ ┊28┊ messageAdded: Message!
+┊ ┊29┊}
Changes are triggered using an event-emitter like object called PubSub
. This can be done using the PubSub.prototype.publish
method. We will create a new instance of it and will provide it via the context - a common pattern for providing objects which are useful for the execution of the resolvers:
TODO: Explain what the context is
@@ -1,4 +1,4 @@
-┊1┊ ┊import { ApolloServer, gql } from 'apollo-server-express';
+┊ ┊1┊import { ApolloServer, gql, PubSub } from 'apollo-server-express';
┊2┊2┊import cors from 'cors';
┊3┊3┊import express from 'express';
┊4┊4┊import schema from './schema';
@@ -12,7 +12,11 @@
┊12┊12┊ res.send('pong');
┊13┊13┊});
┊14┊14┊
-┊15┊ ┊const server = new ApolloServer({ schema });
+┊ ┊15┊const pubsub = new PubSub();
+┊ ┊16┊const server = new ApolloServer({
+┊ ┊17┊ schema,
+┊ ┊18┊ context: () => ({ pubsub }),
+┊ ┊19┊});
┊16┊20┊
┊17┊21┊server.applyMiddleware({
┊18┊22┊ app,
Inside the addMessage
resolver we will publish a new event called messageAdded
. The 3rd argument of the resolver will be the context object that we've just defined in the previous step, where we can use the pubsub instance. The TypeScript type of our context can be directly defined and generated by CodeGen through the codegen.yml
file. This can be specified under the ContextType
field with the file path that contains the context followed by the name of the exported object, like so:
@@ -6,6 +6,7 @@
┊ 6┊ 6┊ - typescript
┊ 7┊ 7┊ - typescript-resolvers
┊ 8┊ 8┊ config:
+┊ ┊ 9┊ contextType: ../context#MyContext
┊ 9┊10┊ mappers:
┊10┊11┊ # import { Message } from '../db'
┊11┊12┊ # The root types of Message resolvers
@@ -0,0 +1,5 @@
+┊ ┊1┊import { PubSub } from 'apollo-server-express';
+┊ ┊2┊
+┊ ┊3┊export type MyContext = {
+┊ ┊4┊ pubsub: PubSub;
+┊ ┊5┊};
The event will be published right after the message was pushed into the messages collection, because order is a crucial thing. We don't want to notify our users unless the change has been made. The event will have a single parameter which represents the new message.
@@ -29,7 +29,7 @@
┊29┊29┊ },
┊30┊30┊
┊31┊31┊ Mutation: {
-┊32┊ ┊ addMessage(root, { chatId, content }) {
+┊ ┊32┊ addMessage(root, { chatId, content }, { pubsub }) {
┊33┊33┊ const chatIndex = chats.findIndex((c) => c.id === chatId);
┊34┊34┊
┊35┊35┊ if (chatIndex === -1) return null;
@@ -52,6 +52,10 @@
┊52┊52┊ chats.splice(chatIndex, 1);
┊53┊53┊ chats.unshift(chat);
┊54┊54┊
+┊ ┊55┊ pubsub.publish('messageAdded', {
+┊ ┊56┊ messageAdded: message,
+┊ ┊57┊ });
+┊ ┊58┊
┊55┊59┊ return message;
┊56┊60┊ },
┊57┊61┊ },
@@ -1,5 +1,5 @@
┊1┊1┊import { createTestClient } from 'apollo-server-testing';
-┊2┊ ┊import { ApolloServer, gql } from 'apollo-server-express';
+┊ ┊2┊import { ApolloServer, PubSub, gql } from 'apollo-server-express';
┊3┊3┊import schema from '../../schema';
┊4┊4┊import { resetDb } from '../../db';
┊5┊5┊
@@ -7,7 +7,10 @@
┊ 7┊ 7┊ beforeEach(resetDb);
┊ 8┊ 8┊
┊ 9┊ 9┊ it('should add message to specified chat', async () => {
-┊10┊ ┊ const server = new ApolloServer({ schema });
+┊ ┊10┊ const server = new ApolloServer({
+┊ ┊11┊ schema,
+┊ ┊12┊ context: () => ({ pubsub: new PubSub() }),
+┊ ┊13┊ });
┊11┊14┊
┊12┊15┊ const { query, mutate } = createTestClient(server);
A subscription resolver behaves differently and thus should be implemented differently. Using the pubsub.asyncIterator
instance, we can specify which events are relevant for the subscription, for example, all clients who are subscribers of the chatUpdated
subscription will be notified when messageAdded
, messageRemoved
and chatInfoChanged
events were triggered. For now, we will have a 1 to 1 relationship between the messageAdded
event and messageAdded
subscription. In code, it should look like this:
@@ -59,6 +59,13 @@
┊59┊59┊ return message;
┊60┊60┊ },
┊61┊61┊ },
+┊ ┊62┊
+┊ ┊63┊ Subscription: {
+┊ ┊64┊ messageAdded: {
+┊ ┊65┊ subscribe: (root, args, { pubsub }) =>
+┊ ┊66┊ pubsub.asyncIterator('messageAdded'),
+┊ ┊67┊ },
+┊ ┊68┊ },
┊62┊69┊};
┊63┊70┊
┊64┊71┊export default resolvers;
The idea behind the pubsub.asyncIterator
method is that it returns an Iterator
like object, where each value is a promise that will be resolved when the relevant events are triggered. By default, the parameter that has a similar name to the subscription will be returned as a response, e.g. messageAdded
parameter will be sent back to the subscribers. This behavior can be modified as explained here, but it's very unlikely and not necessary for our use case.
As mentioned at the beginning of this article, there needs to be an open connection between the client and the server so live updates can happen. There are serveral methods for doing so, but the 2 most popular ones are:
- Based on polling with HTTP protocol
- Based on web-sockets (WS protocol)
HTTP polling means that each amount of time an HTTP request will be made to the server where potential changes can be sent back to us at any given time. HTTP requests are very reliable, but the problem with them is that they contain a lot of information in their headers, so even if we sent an empty request, it might be still very heavy due to cookies, user-agent, language, request type, etc.
With web-sockets, once a connection has been established, it will remain open and it will only send the information which is relevant for the current session, so it's much faster. The communication between the server and the client is bi-directional when it comes to web-sockets, which means that a user can spontaneously receive information from the server, as long as the communication channel remains open.
More information about the advantages of Web Sockets over HTTP can be found at websocket.org
The subscription mechanism can be installed using the server.installSubscriptionHandlers
. It will use the WS protocol by default and will fallback to HTTP polling if there were troubles establishing a connection via WS protocol:
@@ -1,6 +1,7 @@
┊1┊1┊import { ApolloServer, gql, PubSub } from 'apollo-server-express';
┊2┊2┊import cors from 'cors';
┊3┊3┊import express from 'express';
+┊ ┊4┊import http from 'http';
┊4┊5┊import schema from './schema';
┊5┊6┊
┊6┊7┊const app = express();
@@ -23,8 +24,11 @@
┊23┊24┊ path: '/graphql',
┊24┊25┊});
┊25┊26┊
+┊ ┊27┊const httpServer = http.createServer(app);
+┊ ┊28┊server.installSubscriptionHandlers(httpServer);
+┊ ┊29┊
┊26┊30┊const port = process.env.PORT || 4000;
┊27┊31┊
-┊28┊ ┊app.listen(port, () => {
+┊ ┊32┊httpServer.listen(port, () => {
┊29┊33┊ console.log(`Server is listening on port ${port}`);
┊30┊34┊});
Now we have everything set and we can start listening to subscriptions and react to to triggered changes.
Using subscriptions
To support subscriptions we need to establish a WS connection. For that we will need to update our Apollo client. We will install a couple of packages that will enable such feature:
$ yarn add subscriptions-transport-ws apollo-link apollo-link-ws apollo-utilities
subscriptions-transport-ws
- a transport layer that understands how client and GraphQL API communicates with each other. The spec has GQL_INIT GQL_UPDATE GQL_DATA events.apollo-link-ws
- Will establish a WS connection.apollo-link
- Will enable WS and HTTP connections co-exist in a single client.apollo-utilities
- Includes utility functions that will help us analyze a GraphQL AST.
The WS url can be composed by simply running a regular expression over the REACT_APP_SERVER_URL
environment variable and is unnecessary to be stored separately. Here's how our new client should look like: \
@@ -1,16 +1,48 @@
┊ 1┊ 1┊import { InMemoryCache } from 'apollo-cache-inmemory';
┊ 2┊ 2┊import { ApolloClient } from 'apollo-client';
+┊ ┊ 3┊import { getMainDefinition } from 'apollo-utilities';
┊ 3┊ 4┊import { HttpLink } from 'apollo-link-http';
+┊ ┊ 5┊import { WebSocketLink } from 'apollo-link-ws';
+┊ ┊ 6┊import { ApolloLink, split } from 'apollo-link';
┊ 4┊ 7┊
┊ 5┊ 8┊const httpUri = process.env.REACT_APP_SERVER_URL + '/graphql';
+┊ ┊ 9┊const wsUri = httpUri.replace(/^https?/, 'ws');
┊ 6┊10┊
┊ 7┊11┊const httpLink = new HttpLink({
┊ 8┊12┊ uri: httpUri,
┊ 9┊13┊});
┊10┊14┊
+┊ ┊15┊const wsLink = new WebSocketLink({
+┊ ┊16┊ uri: wsUri,
+┊ ┊17┊ options: {
+┊ ┊18┊ // Automatic reconnect in case of connection error
+┊ ┊19┊ reconnect: true,
+┊ ┊20┊ },
+┊ ┊21┊});
+┊ ┊22┊
+┊ ┊23┊/**
+┊ ┊24┊ * Fix error typing in `split` method in `apollo-link`
+┊ ┊25┊ * Related issue https://github.com/apollographql/apollo-client/issues/3090
+┊ ┊26┊ */
+┊ ┊27┊export interface Definition {
+┊ ┊28┊ kind: string;
+┊ ┊29┊ operation?: string;
+┊ ┊30┊}
+┊ ┊31┊const terminatingLink = split(
+┊ ┊32┊ ({ query }) => {
+┊ ┊33┊ const { kind, operation }: Definition = getMainDefinition(query);
+┊ ┊34┊ // If this is a subscription query, use wsLink, otherwise use httpLink
+┊ ┊35┊ return kind === 'OperationDefinition' && operation === 'subscription';
+┊ ┊36┊ },
+┊ ┊37┊ wsLink,
+┊ ┊38┊ httpLink
+┊ ┊39┊);
+┊ ┊40┊
+┊ ┊41┊const link = ApolloLink.from([terminatingLink]);
+┊ ┊42┊
┊11┊43┊const inMemoryCache = new InMemoryCache();
┊12┊44┊
┊13┊45┊export default new ApolloClient({
-┊14┊ ┊ link: httpLink,
+┊ ┊46┊ link,
┊15┊47┊ cache: inMemoryCache,
┊16┊48┊});
Our subscription listeners should live globally across our application and shouldn't be bound to a specific component, thus we will create an external service which will be responsible of doing so. Using that service, we will update our GraphQL data-store any time a new message has been added. We will define a messageAdded
subscription in a dedicated file under the src/graphql/subscriptions
dir where all our subscriptions will be defined and exported:
@@ -0,0 +1 @@
+┊ ┊1┊export { default as messageAdded } from './messageAdded.subscription';
@@ -0,0 +1,11 @@
+┊ ┊ 1┊import gql from 'graphql-tag';
+┊ ┊ 2┊import * as fragments from '../fragments';
+┊ ┊ 3┊
+┊ ┊ 4┊export default gql`
+┊ ┊ 5┊ subscription MessageAdded {
+┊ ┊ 6┊ messageAdded {
+┊ ┊ 7┊ ...Message
+┊ ┊ 8┊ }
+┊ ┊ 9┊ }
+┊ ┊10┊ ${fragments.message}
+┊ ┊11┊`;
Now we will create the service under the path services/cache.service.ts
.
Like any other GraphQL operation, @apollo/react-hooks
provides us with a dedicated React hook for subscriptions called useSubscription
.
Given the subscription document and the onSubscriptionData
callback we can handle incoming changes.
We will be using GraphQL Code Generator to generate typed subscription hooks, as the typescript-react-apollo
plug-in supports it right out of the box.
So let's run the code generation command:
$ yarn codegen
Now we can import and use the newly generated hook useMessageAddedSubscription
in the cache.service
. Like mentioned earlier, we will be using the onSubscriptionData
callback to retrieve the change that was sent by the server and we will use it to re-write our cache. In this case we will be writing a new fragment for the incoming message, and we will update the correlated chat:
@@ -0,0 +1,92 @@
+┊ ┊ 1┊import { DataProxy } from 'apollo-cache';
+┊ ┊ 2┊import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
+┊ ┊ 3┊import * as fragments from '../graphql/fragments';
+┊ ┊ 4┊import * as queries from '../graphql/queries';
+┊ ┊ 5┊import {
+┊ ┊ 6┊ MessageFragment,
+┊ ┊ 7┊ useMessageAddedSubscription,
+┊ ┊ 8┊ ChatsQuery,
+┊ ┊ 9┊} from '../graphql/types';
+┊ ┊10┊
+┊ ┊11┊type Client = Pick<
+┊ ┊12┊ DataProxy,
+┊ ┊13┊ 'readFragment' | 'writeFragment' | 'readQuery' | 'writeQuery'
+┊ ┊14┊>;
+┊ ┊15┊
+┊ ┊16┊export const useCacheService = () => {
+┊ ┊17┊ useMessageAddedSubscription({
+┊ ┊18┊ onSubscriptionData: ({ client, subscriptionData: { data } }) => {
+┊ ┊19┊ if (data) {
+┊ ┊20┊ writeMessage(client, data.messageAdded);
+┊ ┊21┊ }
+┊ ┊22┊ },
+┊ ┊23┊ });
+┊ ┊24┊};
+┊ ┊25┊
+┊ ┊26┊export const writeMessage = (client: Client, message: MessageFragment) => {
+┊ ┊27┊ type FullChat = { [key: string]: any };
+┊ ┊28┊ let fullChat;
+┊ ┊29┊
+┊ ┊30┊ const chatIdFromStore = defaultDataIdFromObject(message.chat);
+┊ ┊31┊
+┊ ┊32┊ if (chatIdFromStore === null) {
+┊ ┊33┊ return;
+┊ ┊34┊ }
+┊ ┊35┊ try {
+┊ ┊36┊ fullChat = client.readFragment<FullChat>({
+┊ ┊37┊ id: chatIdFromStore,
+┊ ┊38┊ fragment: fragments.fullChat,
+┊ ┊39┊ fragmentName: 'FullChat',
+┊ ┊40┊ });
+┊ ┊41┊ } catch (e) {
+┊ ┊42┊ return;
+┊ ┊43┊ }
+┊ ┊44┊
+┊ ┊45┊ if (fullChat === null || fullChat.messages === null) {
+┊ ┊46┊ return;
+┊ ┊47┊ }
+┊ ┊48┊ if (fullChat.messages.some((m: any) => m.id === message.id)) return;
+┊ ┊49┊
+┊ ┊50┊ fullChat.messages.push(message);
+┊ ┊51┊ fullChat.lastMessage = message;
+┊ ┊52┊
+┊ ┊53┊ client.writeFragment({
+┊ ┊54┊ id: chatIdFromStore,
+┊ ┊55┊ fragment: fragments.fullChat,
+┊ ┊56┊ fragmentName: 'FullChat',
+┊ ┊57┊ data: fullChat,
+┊ ┊58┊ });
+┊ ┊59┊
+┊ ┊60┊ let data;
+┊ ┊61┊ try {
+┊ ┊62┊ data = client.readQuery<ChatsQuery>({
+┊ ┊63┊ query: queries.chats,
+┊ ┊64┊ });
+┊ ┊65┊ } catch (e) {
+┊ ┊66┊ return;
+┊ ┊67┊ }
+┊ ┊68┊
+┊ ┊69┊ if (!data || data === null) {
+┊ ┊70┊ return null;
+┊ ┊71┊ }
+┊ ┊72┊ if (!data.chats || data.chats === undefined) {
+┊ ┊73┊ return null;
+┊ ┊74┊ }
+┊ ┊75┊ const chats = data.chats;
+┊ ┊76┊
+┊ ┊77┊ const chatIndex = chats.findIndex((c: any) => {
+┊ ┊78┊ if (message === null || message.chat === null) return -1;
+┊ ┊79┊ return c.id === message?.chat?.id;
+┊ ┊80┊ });
+┊ ┊81┊ if (chatIndex === -1) return;
+┊ ┊82┊ const chatWhereAdded = chats[chatIndex];
+┊ ┊83┊
+┊ ┊84┊ // The chat will appear at the top of the ChatsList component
+┊ ┊85┊ chats.splice(chatIndex, 1);
+┊ ┊86┊ chats.unshift(chatWhereAdded);
+┊ ┊87┊
+┊ ┊88┊ client.writeQuery({
+┊ ┊89┊ query: queries.chats,
+┊ ┊90┊ data: { chats: chats },
+┊ ┊91┊ });
+┊ ┊92┊};
We will also use the exported writeMessage()
function in the ChatRoomScreen
so we won't have any code duplications:
@@ -1,4 +1,3 @@
-┊1┊ ┊import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
┊2┊1┊import gql from 'graphql-tag';
┊3┊2┊import React from 'react';
┊4┊3┊import { useCallback } from 'react';
@@ -7,13 +6,9 @@
┊ 7┊ 6┊import MessageInput from './MessageInput';
┊ 8┊ 7┊import MessagesList from './MessagesList';
┊ 9┊ 8┊import { History } from 'history';
-┊10┊ ┊import {
-┊11┊ ┊ ChatsQuery,
-┊12┊ ┊ useGetChatQuery,
-┊13┊ ┊ useAddMessageMutation,
-┊14┊ ┊} from '../../graphql/types';
-┊15┊ ┊import * as queries from '../../graphql/queries';
+┊ ┊ 9┊import { useGetChatQuery, useAddMessageMutation } from '../../graphql/types';
┊16┊10┊import * as fragments from '../../graphql/fragments';
+┊ ┊11┊import { writeMessage } from '../../services/cache.service';
┊17┊12┊
┊18┊13┊const Container = styled.div`
┊19┊14┊ background: url(/assets/chat-background.jpg);
@@ -47,10 +42,6 @@
┊47┊42┊ history: History;
┊48┊43┊}
┊49┊44┊
-┊50┊ ┊interface ChatsResult {
-┊51┊ ┊ chats: any[];
-┊52┊ ┊}
-┊53┊ ┊
┊54┊45┊const ChatRoomScreen: React.FC<ChatRoomScreenParams> = ({
┊55┊46┊ history,
┊56┊47┊ chatId,
@@ -82,73 +73,7 @@
┊ 82┊ 73┊ },
┊ 83┊ 74┊ update: (client, { data }) => {
┊ 84┊ 75┊ if (data && data.addMessage) {
-┊ 85┊ ┊ type FullChat = { [key: string]: any };
-┊ 86┊ ┊ let fullChat;
-┊ 87┊ ┊ const chatIdFromStore = defaultDataIdFromObject(chat);
-┊ 88┊ ┊
-┊ 89┊ ┊ if (chatIdFromStore === null) {
-┊ 90┊ ┊ return;
-┊ 91┊ ┊ }
-┊ 92┊ ┊ try {
-┊ 93┊ ┊ fullChat = client.readFragment<FullChat>({
-┊ 94┊ ┊ id: chatIdFromStore,
-┊ 95┊ ┊ fragment: fragments.fullChat,
-┊ 96┊ ┊ fragmentName: 'FullChat',
-┊ 97┊ ┊ });
-┊ 98┊ ┊ } catch (e) {
-┊ 99┊ ┊ return;
-┊100┊ ┊ }
-┊101┊ ┊
-┊102┊ ┊ if (fullChat === null || fullChat.messages === null) {
-┊103┊ ┊ return;
-┊104┊ ┊ }
-┊105┊ ┊ if (
-┊106┊ ┊ fullChat.messages.some(
-┊107┊ ┊ (currentMessage: any) =>
-┊108┊ ┊ data.addMessage && currentMessage.id === data.addMessage.id
-┊109┊ ┊ )
-┊110┊ ┊ ) {
-┊111┊ ┊ return;
-┊112┊ ┊ }
-┊113┊ ┊
-┊114┊ ┊ fullChat.messages.push(data.addMessage);
-┊115┊ ┊ fullChat.lastMessage = data.addMessage;
-┊116┊ ┊
-┊117┊ ┊ client.writeFragment({
-┊118┊ ┊ id: chatIdFromStore,
-┊119┊ ┊ fragment: fragments.fullChat,
-┊120┊ ┊ fragmentName: 'FullChat',
-┊121┊ ┊ data: fullChat,
-┊122┊ ┊ });
-┊123┊ ┊
-┊124┊ ┊ let clientChatsData: ChatsQuery | null;
-┊125┊ ┊ try {
-┊126┊ ┊ clientChatsData = client.readQuery({
-┊127┊ ┊ query: queries.chats,
-┊128┊ ┊ });
-┊129┊ ┊ } catch (e) {
-┊130┊ ┊ return;
-┊131┊ ┊ }
-┊132┊ ┊
-┊133┊ ┊ if (!clientChatsData || !clientChatsData.chats) {
-┊134┊ ┊ return null;
-┊135┊ ┊ }
-┊136┊ ┊ const chats = clientChatsData.chats;
-┊137┊ ┊
-┊138┊ ┊ const chatIndex = chats.findIndex(
-┊139┊ ┊ (currentChat: any) => currentChat.id === chatId
-┊140┊ ┊ );
-┊141┊ ┊ if (chatIndex === -1) return;
-┊142┊ ┊ const chatWhereAdded = chats[chatIndex];
-┊143┊ ┊
-┊144┊ ┊ // The chat will appear at the top of the ChatsList component
-┊145┊ ┊ chats.splice(chatIndex, 1);
-┊146┊ ┊ chats.unshift(chatWhereAdded);
-┊147┊ ┊
-┊148┊ ┊ client.writeQuery({
-┊149┊ ┊ query: queries.chats,
-┊150┊ ┊ data: { chats: chats },
-┊151┊ ┊ });
+┊ ┊ 76┊ writeMessage(client, data.addMessage);
┊152┊ 77┊ }
┊153┊ 78┊ },
┊154┊ 79┊ });
One thing missing that you might notice is that we're trying to retrieve the chat from the received message, unfortunately our GraphQL schema doesn't support it and we will need to add it. On the server, we will add a chat
field to the Message
type in the GraphQL schema, and we will implement a resolver which will lookup for the chat in the chats collection:
@@ -6,6 +6,12 @@
┊ 6┊ 6┊ Date: DateTimeResolver,
┊ 7┊ 7┊ URL: URLResolver,
┊ 8┊ 8┊
+┊ ┊ 9┊ Message: {
+┊ ┊10┊ chat(message) {
+┊ ┊11┊ return chats.find(c => c.messages.some(m => m === message.id)) || null;
+┊ ┊12┊ },
+┊ ┊13┊ },
+┊ ┊14┊
┊ 9┊15┊ Chat: {
┊10┊16┊ messages(chat) {
┊11┊17┊ return messages.filter((m) => chat.messages.includes(m.id));
@@ -5,6 +5,7 @@
┊ 5┊ 5┊ id: ID!
┊ 6┊ 6┊ content: String!
┊ 7┊ 7┊ createdAt: Date!
+┊ ┊ 8┊ chat: Chat
┊ 8┊ 9┊}
┊ 9┊10┊
┊10┊11┊type Chat {
Now that we have it supported we can update the Message
fragment in the client to include that information. We don't need the entire chat, only its ID, since the fragment ID composition is done out of an ID and type name:
@@ -68,6 +68,10 @@
┊68┊68┊ __typename: 'Message',
┊69┊69┊ id: Math.random().toString(36).substr(2, 9),
┊70┊70┊ createdAt: new Date(),
+┊ ┊71┊ chat: {
+┊ ┊72┊ __typename: 'Chat',
+┊ ┊73┊ id: chatId,
+┊ ┊74┊ },
┊71┊75┊ content,
┊72┊76┊ },
┊73┊77┊ },
@@ -44,6 +44,10 @@
┊44┊44┊ id: 1,
┊45┊45┊ content: 'Hello',
┊46┊46┊ createdAt: new Date('1 Jan 2019 GMT'),
+┊ ┊47┊ chat: {
+┊ ┊48┊ __typename: 'Chat',
+┊ ┊49┊ id: 1,
+┊ ┊50┊ },
┊47┊51┊ },
┊48┊52┊ },
┊49┊53┊ ],
@@ -90,6 +94,10 @@
┊ 90┊ 94┊ id: 1,
┊ 91┊ 95┊ content: 'Hello',
┊ 92┊ 96┊ createdAt: new Date('1 Jan 2019 GMT'),
+┊ ┊ 97┊ chat: {
+┊ ┊ 98┊ __typename: 'Chat',
+┊ ┊ 99┊ id: 1,
+┊ ┊100┊ },
┊ 93┊101┊ },
┊ 94┊102┊ },
┊ 95┊103┊ ],
@@ -5,5 +5,8 @@
┊ 5┊ 5┊ id
┊ 6┊ 6┊ createdAt
┊ 7┊ 7┊ content
+┊ ┊ 8┊ chat {
+┊ ┊ 9┊ id
+┊ ┊10┊ }
┊ 8┊11┊ }
┊ 9┊12┊`;
Finally, we will import the useCacheService
React hook that we've just created and we will use it in our main App
component. This means that the cache service will start listening for changes right as the app component is mounted:
@@ -3,9 +3,15 @@
┊ 3┊ 3┊import ReactDOM from 'react-dom';
┊ 4┊ 4┊import App from './App';
┊ 5┊ 5┊import { mockApolloClient } from './test-helpers';
+┊ ┊ 6┊import * as subscriptions from './graphql/subscriptions';
┊ 6┊ 7┊
┊ 7┊ 8┊it('renders without crashing', () => {
-┊ 8┊ ┊ const client = mockApolloClient();
+┊ ┊ 9┊ const client = mockApolloClient([
+┊ ┊10┊ {
+┊ ┊11┊ request: { query: subscriptions.messageAdded },
+┊ ┊12┊ result: { data: {} }
+┊ ┊13┊ }
+┊ ┊14┊ ]);
┊ 9┊15┊ const div = document.createElement('div');
┊10┊16┊
┊11┊17┊ ReactDOM.render(
@@ -8,26 +8,31 @@
┊ 8┊ 8┊import ChatRoomScreen from './components/ChatRoomScreen';
┊ 9┊ 9┊import ChatsListScreen from './components/ChatsListScreen';
┊10┊10┊import AnimatedSwitch from './components/AnimatedSwitch';
+┊ ┊11┊import { useCacheService } from './services/cache.service';
┊11┊12┊
-┊12┊ ┊const App: React.FC = () => (
-┊13┊ ┊ <BrowserRouter>
-┊14┊ ┊ <AnimatedSwitch>
-┊15┊ ┊ <Route exact path="/chats" component={ChatsListScreen} />
+┊ ┊13┊const App: React.FC = () => {
+┊ ┊14┊ useCacheService();
┊16┊15┊
-┊17┊ ┊ <Route
-┊18┊ ┊ exact
-┊19┊ ┊ path="/chats/:chatId"
-┊20┊ ┊ component={({
-┊21┊ ┊ match,
-┊22┊ ┊ history,
-┊23┊ ┊ }: RouteComponentProps<{ chatId: string }>) => (
-┊24┊ ┊ <ChatRoomScreen chatId={match.params.chatId} history={history} />
-┊25┊ ┊ )}
-┊26┊ ┊ />
-┊27┊ ┊ </AnimatedSwitch>
-┊28┊ ┊ <Route exact path="/" render={redirectToChats} />
-┊29┊ ┊ </BrowserRouter>
-┊30┊ ┊);
+┊ ┊16┊ return (
+┊ ┊17┊ <BrowserRouter>
+┊ ┊18┊ <AnimatedSwitch>
+┊ ┊19┊ <Route exact path="/chats" component={ChatsListScreen} />
+┊ ┊20┊
+┊ ┊21┊ <Route
+┊ ┊22┊ exact
+┊ ┊23┊ path="/chats/:chatId"
+┊ ┊24┊ component={({
+┊ ┊25┊ match,
+┊ ┊26┊ history,
+┊ ┊27┊ }: RouteComponentProps<{ chatId: string }>) => (
+┊ ┊28┊ <ChatRoomScreen chatId={match.params.chatId} history={history} />
+┊ ┊29┊ )}
+┊ ┊30┊ />
+┊ ┊31┊ </AnimatedSwitch>
+┊ ┊32┊ <Route exact path="/" render={redirectToChats} />
+┊ ┊33┊ </BrowserRouter>
+┊ ┊34┊ );
+┊ ┊35┊};
┊31┊36┊
┊32┊37┊const redirectToChats = () => <Redirect to="/chats" />;
Subscription handling is complete! If you'll try to repeat the same process again where you check messages updating between 2 instances of the app, you should see them both update.
TODO: useCacheService
shouldn’t be called like that since it’s related to message events and cache updates are only side-effects.
< Previous Step | Next Step > |
---|