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()
})
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()
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>
)
}
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>
)
}
Login mutation:
import { gql } from 'apollo-boost'
export const LOGIN = gql`
mutation login($email: String!, $password: String!) {
login(email: $email, password: $password)
}
`
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>
)}
</>
)
}
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
}
}
`
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} />
</>
)
}
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}
`
// src/pages/chat/subscription.js
import { gql } from 'apollo-boost'
import { MESSAGE } from './fragments'
export const MESSAGES_SUBSCRIPTION = gql`
subscription messageSent {
messageSent {
...Message
}
}
${MESSAGE}
`
// 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}
`
// 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}
`
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} />
)
}
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>
)
}
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)