Ricardo Borges

Personal blog

Learning GraphQL by building a chat application - Part 2

Continuing the previous article we are going to build our chat application front-end, this article assumes you're familiar with ReactJS, so we'll focus more on GraphQL and Apollo and less in ReactJS, so before we start I suggest you clone the project repository. Also, you'll notice that there is room for improving usability and style, because, as I said, we are more concerned to use GraphQL with Apollo than any other aspect of this application.

Initial setup

Let's get started, we'll develop three features: Login, contacts list, and conversation. The application flow is very simple, the user, after login, will choose a contact in a contacts list to start a conversation and will start sending messages (login > contacts list > chat).

The quick way to start our application would use Apollo Boost, but it doesn't have support for subscriptions, so we need to configure the Apollo Client manually, we'll put all this configuration in api.js file:

1// src/api.js 
2
3import { InMemoryCache } from 'apollo-cache-inmemory'
4import { getMainDefinition } from 'apollo-utilities'
5import { WebSocketLink } from 'apollo-link-ws'
6import { ApolloClient } from 'apollo-client'
7import { HttpLink } from 'apollo-link-http'
8import { split } from 'apollo-link'
9
10/**
11* Web socket configuration that we'll use in our subscriptions
12* We can send connection params in the `options` property, we'll see another way
13* to send these params later
14*/
15const wsLink = new WebSocketLink({
16  uri: process.env.REACT_APP_API_WS_URL,
17  options: {
18    reconnect: true,
19    connectionParams: () => ({
20      Authorization: `Bearer ${localStorage.getItem('token')}`
21    })
22  }
23})
24
25/**
26* HTTP configuration that we'll use in any other request
27*/
28const httpLink = new HttpLink({
29  uri: process.env.REACT_APP_API_URL,
30  // It is possible to set headers here too:
31  headers: {
32    Authorization: `Bearer ${localStorage.getItem('token')}`
33  }
34})
35
36const link = split(({ query }) => {
37  const definition = getMainDefinition(query)
38  return (
39    definition.kind === 'OperationDefinition' &&
40    definition.operation === 'subscription'
41  )
42},
43  wsLink,
44  httpLink
45)
46
47export const client = new ApolloClient({
48  link,
49  cache: new InMemoryCache()
50})

Don't forget to edit the environment variables in .env file to match your local configurations, there are only two, probably you'll use the same values that are in .env.sample file.

Next, in index.js file we import the configured Apollo Client and provide it to <ApolloProvider> component:

1// src/index.js
2
3import React from 'react'
4import ReactDOM from 'react-dom'
5import { ApolloProvider } from '@apollo/react-hooks'
6import * as serviceWorker from './serviceWorker'
7import { client } from './api'
8import { App } from './App'
9
10ReactDOM.render(
11  <ApolloProvider client={client}>
12    <App />
13  </ApolloProvider>,
14  document.getElementById('root')
15)
16
17serviceWorker.unregister()

In the <App> component there are only our routes:

1// src/App.js
2
3import 'milligram'
4import React from 'react'
5import { BrowserRouter, Switch, Route } from 'react-router-dom'
6import { Header } from './components/Header'
7import { Chat } from './pages/chat'
8import { Login } from './pages/login'
9import { Contacts } from './pages/contacts'
10
11export const App = () => {
12  return (
13    <div className='container'>
14      <BrowserRouter forceRefresh={true}>
15        <Header />
16        <Switch>
17          <Route exact path='/' component={Login} />
18          <Route path='/login' component={Login} />
19          <Route path='/contacts' component={Contacts} />
20          <Route path='/chat/:id' component={Chat} />
21        </Switch>
22      </BrowserRouter>
23    </div>
24  )
25}

Apollo Client's React Hooks

Before we continue, some code snippets will have some parts omitted, but I put a link to complete code after the snippet when needed.

Apollo client provides three hooks for queries, mutations, and subscriptions, the first hook we'll use is useMutation on the login page, so the user will enter his email, password and click on the login button, then the LOGIN mutation will be executed:

1// src/pages/login/index.js
2
3import React, { useEffect } from 'react'
4import { useMutation } from '@apollo/react-hooks'
5import { LOGIN } from './mutations'
6
7export const Login = ({ history }) => {
8  let email
9  let password
10  const [login, { data }] = useMutation(LOGIN)
11
12  return (
13    <div className='row'>
14      <div className='column column-50 column-offset-25'>
15        <form>
16          {/* ... */}
17          <div className='row'>
18            <div className='column column-50 column-offset-25'>
19              <button
20                className='float-right'
21                onClick={e => {
22                  e.preventDefault()
23                  login({ variables: { email: email.value, password: password.value } })
24                }}
25              >
26                Login
27              </button>
28            </div>
29          </div>
30        </form>
31      </div>
32    </div>
33  )
34}

login page component

Login mutation:

1import { gql } from 'apollo-boost'
2
3export const LOGIN = gql`
4  mutation login($email: String!, $password: String!) {
5    login(email: $email, password: $password)
6  }
7`

It is simple like that, you call useMutation, pass it a mutation string that represents the mutation and it returns a function and the possible data from the mutation, in this case, login and data, you call the login function with some variables and it is done.

We are not creating a register page, I'll leave this challenge for you, or you can create a user on GraphQL playground.

Moving on to the contacts page we will use the useQuery hook, which is quite straightforward we pass it a GraphQL query string, when the component renders, useQuery returns an object from Apollo Client that contains loading, error, and data properties:

1// src/pages/contacts/index.js
2
3import React from 'react'
4import { useQuery } from '@apollo/react-hooks'
5import { USERS } from './queries'
6
7export const Contacts = ({ history }) => {
8  const { loading, error, data } = useQuery(USERS, {
9    context: {
10      headers: {
11        Authorization: `Bearer ${localStorage.getItem('token')}`
12      }
13    }
14  })
15
16  if (loading) return 'loading ...'
17
18  if (error) return `Error: ${error.message}`
19
20  return (
21    <>
22      {data.users.map(user =>
23        <div key={user.id} className='row'>
24          <div className='column' />
25          <div className='column' style={{ textAlign: 'center' }}>
26            <button
27              className='button button-outline'
28              onClick={() => history.push(`/chat/${user.id}`)}
29            >
30              {user.name}
31            </button>
32          </div>
33          <div className='column' />
34        </div>
35      )}
36    </>
37  )
38}

This time besides the USERS query string we pass it the Bearer token, useQuery, like the other hooks, accepts other arguments, refer to the documentation for more details.

Here is the USERS query:

1// src/pages/contacts/queries.js
2
3import { gql } from 'apollo-boost'
4
5export const USERS = gql`
6  query Users {
7    users {
8      id
9      name
10      email
11    }
12  } 
13`

The next page is the chat page, there are more components on this page then in the others, let's start with the main component:

1// src/pages/chat/index.js
2
3import React from 'react'
4import { useQuery } from '@apollo/react-hooks'
5import { CONVERSATION } from './queries'
6import { MESSAGES_SUBSCRIPTION } from './subscription'
7import { MessageList } from './components/MessageList'
8import { SendForm } from './components/SendForm'
9
10const handleNewMessage = (subscribeToMore) => {
11  subscribeToMore({
12    document: MESSAGES_SUBSCRIPTION,
13    updateQuery: (prev, { subscriptionData }) => {
14      if (!subscriptionData.data) return prev
15      const newMessage = subscriptionData.data.messageSent
16
17      return {
18        conversation: [...prev.conversation, newMessage]
19      }
20    }
21  })
22}
23
24export const Chat = ({ match }) => {
25  const options = {
26    context: {
27      headers: {
28        Authorization: `Bearer ${localStorage.getItem('token')}`
29      }
30    },
31    variables: {
32      cursor: '0',
33      receiverId: match.params.id
34    },
35  }
36
37  const { subscribeToMore, ...result } = useQuery(CONVERSATION, options)
38
39  return (
40    <>
41      <div
42        className='row'
43        style={{
44          height: window.innerHeight - 250,
45          overflowY: 'scroll',
46          marginBottom: 10
47        }}>
48        <div className='column'>
49          <MessageList
50            {...result}
51            subscribeToNewMessages={() => handleNewMessage(subscribeToMore)}
52          />
53        </div>
54      </div>
55      <SendForm receiverId={match.params.id} />
56    </>
57  )
58}

Every time a user sends a message we want to show that message along with the previous ones, to do that we can use the function subscribeToMore that is available on every query result and will be called every time the subscription returns. The function handleNewMessage will handle the new messages inserting them into the list of messages.

Below are the GraphQL queries, mutations, subscriptions, and fragments used in the chat page, a fragment is a shared piece of query logic:

1// src/pages/chat/queries.js
2
3import { gql } from 'apollo-boost'
4import { MESSAGE } from './fragments'
5
6export const MESSAGES = gql`
7  query Messages($cursor: String!) {
8    messages(cursor: $cursor) {
9      ...Message
10    }
11  } 
12  ${MESSAGE}
13`
14
15export const CONVERSATION = gql`
16  query Conversation($cursor: String!, $receiverId: ID!) {
17    conversation(cursor: $cursor, receiverId: $receiverId) {
18      ...Message
19    }
20  } 
21  ${MESSAGE}
22`
1// src/pages/chat/subscription.js
2
3import { gql } from 'apollo-boost'
4import { MESSAGE } from './fragments'
5
6export const MESSAGES_SUBSCRIPTION = gql`
7  subscription messageSent {
8    messageSent {
9      ...Message
10    }
11  }
12  ${MESSAGE}
13`
1// src/pages/chat/mutations.js
2
3import { gql } from 'apollo-boost'
4import { MESSAGE } from './fragments'
5
6export const SEND_MESSAGE = gql`
7  mutation sendMessage($sendMessageInput: SendMessageInput!) {
8    sendMessage(sendMessageInput: $sendMessageInput){
9      ...Message
10    }
11  }
12  ${MESSAGE}
13`
1// src/pages/chat/fragments.js
2
3import { gql } from 'apollo-boost'
4
5export const USER = gql`
6  fragment User on User {
7    id
8    name
9    email
10  }
11`
12
13export const MESSAGE = gql`
14  fragment Message on Message {
15    id
16    message
17    sender {
18      ...User
19    }
20    receiver {
21      ...User
22    }
23  }
24  ${USER}
25`

The MessageList component is responsible for rendering the messages:

1// src/pages/chat/components/MessageList.js
2
3import React, { useEffect, useState } from 'react'
4import { MessageItemSender } from './MessageItemSender'
5import { MessageItemReceiver } from './MessageItemReceiver'
6import { decode } from '../../../session'
7
8export const MessageList = (props) => {
9  const [user, setUser] = useState(null)
10
11  useEffect(() => {
12    setUser(decode())
13    props.subscribeToNewMessages()
14  }, [])
15
16  if (!props.data) { return <p>loading...</p> }
17
18  return props.data.conversation.map(message =>
19    user.id === parseInt(message.sender.id, 10)
20      ? <MessageItemSender key={message.id} message={message} />
21      : <MessageItemReceiver key={message.id} message={message} />
22  )
23}

You can find the MessageItemSender and MessageItemReceiver here.

The last component is the SendForm it is responsible for sending messages, and its behavior is similar to the login component:

1// src/pages/chat/components/SendForm.js
2
3import React from 'react'
4import { useMutation } from '@apollo/react-hooks'
5import { SEND_MESSAGE } from '../mutations'
6
7export const SendForm = ({ receiverId }) => {
8  let input
9  const [sendMessage] = useMutation(SEND_MESSAGE)
10
11  return (
12    <div className='row'>
13      <div className='column column-80'>
14        <input type='text' ref={node => { input = node }} />
15      </div>
16      <div className='column column-20'>
17        <button onClick={e => {
18          e.preventDefault()
19          sendMessage({
20            variables: {
21              sendMessageInput: {
22                receiverId,
23                message: input.value
24              }
25            }
26          })
27        }}
28        >
29          Send
30      </button>
31      </div>
32    </div>
33  )
34}

This is it, to see the app working you could create two users and login with each account in different browsers and send messages to each other.