DEV Community

Cover image for Learning GraphQL by building a chat application - Part 2
Ricardo Borges
Ricardo Borges

Posted on • Edited on

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 on 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:

// src/api.js 

import { InMemoryCache } from 'apollo-cache-inmemory'
import { getMainDefinition } from 'apollo-utilities'
import { WebSocketLink } from 'apollo-link-ws'
import { ApolloClient } from 'apollo-client'
import { HttpLink } from 'apollo-link-http'
import { split } from 'apollo-link'

/**
* Web socket configuration that we'll use in our subscriptions
* We can send connection params in the `options` property, we'll see another way
* to send these params later
*/
const wsLink = new WebSocketLink({
  uri: process.env.REACT_APP_API_WS_URL,
  options: {
    reconnect: true,
    connectionParams: () => ({
      Authorization: `Bearer ${localStorage.getItem('token')}`
    })
  }
})

/**
* HTTP configuration that we'll use in any other request
*/
const httpLink = new HttpLink({
  uri: process.env.REACT_APP_API_URL,
  // It is possible to set headers here too:
  headers: {
    Authorization: `Bearer ${localStorage.getItem('token')}`
  }
})

const link = split(({ query }) => {
  const definition = getMainDefinition(query)
  return (
    definition.kind === 'OperationDefinition' &&
    definition.operation === 'subscription'
  )
},
  wsLink,
  httpLink
)

export const client = new ApolloClient({
  link,
  cache: new InMemoryCache()
})

Enter fullscreen mode Exit fullscreen mode

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

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

// src/index.js

import React from 'react'
import ReactDOM from 'react-dom'
import { ApolloProvider } from '@apollo/react-hooks'
import * as serviceWorker from './serviceWorker'
import { client } from './api'
import { App } from './App'

ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>,
  document.getElementById('root')
)

serviceWorker.unregister()
Enter fullscreen mode Exit fullscreen mode

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

// src/App.js

import 'milligram'
import React from 'react'
import { BrowserRouter, Switch, Route } from 'react-router-dom'
import { Header } from './components/Header'
import { Chat } from './pages/chat'
import { Login } from './pages/login'
import { Contacts } from './pages/contacts'

export const App = () => {
  return (
    <div className='container'>
      <BrowserRouter forceRefresh={true}>
        <Header />
        <Switch>
          <Route exact path='/' component={Login} />
          <Route path='/login' component={Login} />
          <Route path='/contacts' component={Contacts} />
          <Route path='/chat/:id' component={Chat} />
        </Switch>
      </BrowserRouter>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

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:

// src/pages/login/index.js

import React, { useEffect } from 'react'
import { useMutation } from '@apollo/react-hooks'
import { LOGIN } from './mutations'

export const Login = ({ history }) => {
  let email
  let password
  const [login, { data }] = useMutation(LOGIN)

  return (
    <div className='row'>
      <div className='column column-50 column-offset-25'>
        <form>
          {/* ... */}
          <div className='row'>
            <div className='column column-50 column-offset-25'>
              <button
                className='float-right'
                onClick={e => {
                  e.preventDefault()
                  login({ variables: { email: email.value, password: password.value } })
                }}
              >
                Login
              </button>
            </div>
          </div>
        </form>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

login page component

Login mutation:

import { gql } from 'apollo-boost'

export const LOGIN = gql`
  mutation login($email: String!, $password: String!) {
    login(email: $email, password: $password)
  }
`
Enter fullscreen mode Exit fullscreen mode

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:

// src/pages/contacts/index.js

import React from 'react'
import { useQuery } from '@apollo/react-hooks'
import { USERS } from './queries'

export const Contacts = ({ history }) => {
  const { loading, error, data } = useQuery(USERS, {
    context: {
      headers: {
        Authorization: `Bearer ${localStorage.getItem('token')}`
      }
    }
  })

  if (loading) return 'loading ...'

  if (error) return `Error: ${error.message}`

  return (
    <>
      {data.users.map(user =>
        <div key={user.id} className='row'>
          <div className='column' />
          <div className='column' style={{ textAlign: 'center' }}>
            <button
              className='button button-outline'
              onClick={() => history.push(`/chat/${user.id}`)}
            >
              {user.name}
            </button>
          </div>
          <div className='column' />
        </div>
      )}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

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:

// src/pages/contacts/queries.js

import { gql } from 'apollo-boost'

export const USERS = gql`
  query Users {
    users {
      id
      name
      email
    }
  } 
`
Enter fullscreen mode Exit fullscreen mode

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

// src/pages/chat/index.js

import React from 'react'
import { useQuery } from '@apollo/react-hooks'
import { CONVERSATION } from './queries'
import { MESSAGES_SUBSCRIPTION } from './subscription'
import { MessageList } from './components/MessageList'
import { SendForm } from './components/SendForm'

const handleNewMessage = (subscribeToMore) => {
  subscribeToMore({
    document: MESSAGES_SUBSCRIPTION,
    updateQuery: (prev, { subscriptionData }) => {
      if (!subscriptionData.data) return prev
      const newMessage = subscriptionData.data.messageSent

      return {
        conversation: [...prev.conversation, newMessage]
      }
    }
  })
}

export const Chat = ({ match }) => {
  const options = {
    context: {
      headers: {
        Authorization: `Bearer ${localStorage.getItem('token')}`
      }
    },
    variables: {
      cursor: '0',
      receiverId: match.params.id
    },
  }

  const { subscribeToMore, ...result } = useQuery(CONVERSATION, options)

  return (
    <>
      <div
        className='row'
        style={{
          height: window.innerHeight - 250,
          overflowY: 'scroll',
          marginBottom: 10
        }}>
        <div className='column'>
          <MessageList
            {...result}
            subscribeToNewMessages={() => handleNewMessage(subscribeToMore)}
          />
        </div>
      </div>
      <SendForm receiverId={match.params.id} />
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

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:

// src/pages/chat/queries.js

import { gql } from 'apollo-boost'
import { MESSAGE } from './fragments'

export const MESSAGES = gql`
  query Messages($cursor: String!) {
    messages(cursor: $cursor) {
      ...Message
    }
  } 
  ${MESSAGE}
`

export const CONVERSATION = gql`
  query Conversation($cursor: String!, $receiverId: ID!) {
    conversation(cursor: $cursor, receiverId: $receiverId) {
      ...Message
    }
  } 
  ${MESSAGE}
`
Enter fullscreen mode Exit fullscreen mode
// src/pages/chat/subscription.js

import { gql } from 'apollo-boost'
import { MESSAGE } from './fragments'

export const MESSAGES_SUBSCRIPTION = gql`
  subscription messageSent {
    messageSent {
      ...Message
    }
  }
  ${MESSAGE}
`
Enter fullscreen mode Exit fullscreen mode
// src/pages/chat/mutations.js

import { gql } from 'apollo-boost'
import { MESSAGE } from './fragments'

export const SEND_MESSAGE = gql`
  mutation sendMessage($sendMessageInput: SendMessageInput!) {
    sendMessage(sendMessageInput: $sendMessageInput){
      ...Message
    }
  }
  ${MESSAGE}
`
Enter fullscreen mode Exit fullscreen mode
// src/pages/chat/fragments.js

import { gql } from 'apollo-boost'

export const USER = gql`
  fragment User on User {
    id
    name
    email
  }
`

export const MESSAGE = gql`
  fragment Message on Message {
    id
    message
    sender {
      ...User
    }
    receiver {
      ...User
    }
  }
  ${USER}
`
Enter fullscreen mode Exit fullscreen mode

The MessageList component is responsible for rendering the messages:

// src/pages/chat/components/MessageList.js

import React, { useEffect, useState } from 'react'
import { MessageItemSender } from './MessageItemSender'
import { MessageItemReceiver } from './MessageItemReceiver'
import { decode } from '../../../session'

export const MessageList = (props) => {
  const [user, setUser] = useState(null)

  useEffect(() => {
    setUser(decode())
    props.subscribeToNewMessages()
  }, [])

  if (!props.data) { return <p>loading...</p> }

  return props.data.conversation.map(message =>
    user.id === parseInt(message.sender.id, 10)
      ? <MessageItemSender key={message.id} message={message} />
      : <MessageItemReceiver key={message.id} message={message} />
  )
}
Enter fullscreen mode Exit fullscreen mode

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:

// src/pages/chat/components/SendForm.js

import React from 'react'
import { useMutation } from '@apollo/react-hooks'
import { SEND_MESSAGE } from '../mutations'

export const SendForm = ({ receiverId }) => {
  let input
  const [sendMessage] = useMutation(SEND_MESSAGE)

  return (
    <div className='row'>
      <div className='column column-80'>
        <input type='text' ref={node => { input = node }} />
      </div>
      <div className='column column-20'>
        <button onClick={e => {
          e.preventDefault()
          sendMessage({
            variables: {
              sendMessageInput: {
                receiverId,
                message: input.value
              }
            }
          })
        }}
        >
          Send
      </button>
      </div>
    </div>
  )
}

Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)