Handle and track chat messages delivery using React

Ilyass Ben Hakim
September 2nd, 2021 · 2 min read

Make your chat look like Whatsapp

In this article, we’ll be talking about tracking messages delivery inside a chat application using React.

API

To begin, we will need a ready backend, in our case we have already a graphQL API that has a createMessage mutation (similar to a POST request in RESTful architectures), and that’s using React Apollo GraphQL.

We have also a function called resetForm which will reset our input and clear it from the sent message.

Usage

1...
2const { createMessage } = useMutation(CREATE_MESSAGE);
3
4const handleSendMessage = (message: string) =>
5 createMessage({ variables: { message } })
6 .then(res => /** use the res to update the old messages array */)
7 .catch(() => /** throw an error */)
8...

Regular way of sending a message

The regular way to make a SendMessageInput component, is when the user clicks on send, we call and wait for createMessage response and then we update the messages state by adding the new message.

1...
2const [messages, setMessages] = useState([])
3const { createMessage } = useMutation(CREATE_MESSAGE);
4
5const handleSendMessage = (message:string) =>
6createMessage({ variables: { message }})
7 .then(res => {
8 setMessages(prev => [...prev, res.message]);
9 resetForm();
10 })
11 .catch(() => /** throw an error */)
12...
Regular way of sending a message

The main idea behind my solution is to not wait for the response of the mutation, but instead, we update the messages state by adding the new message once we click send.

But wait!! Each message has a unique id, how can we deal with that 🤔??

Generating a unique ID

Let’s have a function that creates every time a different (unique) id, so we put this fake id in the new message object and we use it as a reference to access that message later when the response comes back.

Math.random should be unique because of its seeding algorithm, we convert it to base 36 (number + letters), and we grab the first 9 characters after the decimal, we add a flag as a string just to make it easier for debugging.

And here we can generate over 10 thousands unique ids at once.

1const generateUniqueId = (flag = '_') => flag + Math.random().toString(36).substr(2, 9);

Usage

1generateUniqueId('newMessage_');
2// result: newMessage_3j2i29d

Replace

And also we will need an immutable function that replace an item inside an array using the index, just to keep the code clean and readable.

1const replace = (array: any[], index: number, element: any) => [...array.slice(0, index), element, ...array.slice(index + 1)];

Usage

1const array = ['JS', 'JAVA', 'PYTHON'];
2replace(array, 1, 'TS');
3// result: ['JS', 'TS', 'PYTHON']

Status

And to make more sense let’s add a property called status to the message object, which will help us tell the user if the message is sent, pending, or has an error. And while rendering the component we can show the status to the user, the same way as I did in my case.

Let’s assume the following object is the message.

1{
2 id : string;
3 sender: string;
4 body: string;
5 createdAt: date;
6 status: 'PENDING' | 'SENT' | 'ERROR'; // The new added property
7}

Implementation

Utils

We can put these tiny helpers into a folder called utils, libs or whatever.

1const generateUniqueId = (flag = '_') => flag + Math.random().toString(36).substr(2, 9);
2const replace = (array: any[], index: number, element: any) => [...array.slice(0, index), element, ...array.slice(index + 1)];

SendMessageInput component

1...
2const currentUser = useCurrentUser()
3const [messages, setMessages] = useState([])
4const { createMessage } = useMutation(CREATE_MESSAGE);
5
6const handleSendMessage = (message: string) => {
7// new message with fake temporary id
8const draftNewMessage = {
9 id : generateUniqueId('newMessage_'),
10 sender: currentUser.id,
11 body: message,
12 createdAt: new Date(),
13 status: 'PENDING'
14}
15
16// add the new message once the user hits send
17setMessages(prev => [...prev, draftNewMessage]);
18
19// clear the form input once the user hits send
20resetForm();
21
22// send the message to the backend
23createMessage({ variables: { message } })
24.then(res => {
25 setMessages((prev) => {
26 // find the index of the fake added message
27 const foundIndex = prev.findIndex((item) => item.id == id);
28
29 // replace it with the original one, that we got from the response
30 return replace(prev, foundIndex, res.message);
31 });
32})
33.catch(() => /** throw an error */)}
34...
Spamming messages

It looks good, isn’t it? But what about if a network issue happens at the moment the user hits send.

Error handling

In the error handling phase, we should think only about how to inform the user that we failed to send the message.

Solution

Let’s convert the draftNewMessage object to be a function that takes status as param and returns the same object, so we can control the status for error handling, at the execution time.

1const draftNewMessage = (status) => ({
2 id : generateUniqueId('newMessage_'),
3 sender: currentUser.id,
4 body: message,
5 createdAt: new Date(),
6 status
7})

Let’s have a look into it now:

1...
2const currentUser = useCurrentUser()
3const [messages, setMessages] = useState([])
4const { createMessage } = useMutation(CREATE_MESSAGE);
5
6const handleSendMessage = (message: string) => {
7
8// generating the new ID
9const id = generateUniqueId('_new_message__');
10
11// new message with fake temporary id
12const draftNewMessage = (status) => ({
13 id,
14 sender: currentUser.id,
15 body: message,
16 createdAt: new Date(),
17 status
18})
19
20// add the new message once the user hits send
21setMessages(prev => [...prev, draftNewMessage('PENDING')]);
22
23// clear the form input once the user hits send
24resetForm();
25
26// send the message to the backend
27createMessage({variables: { message }})
28.then(res => {
29 setMessages((prev) => {
30 // find the index of the fake added message
31 const foundIndex = prev.findIndex((item) => item.id == id);
32
33 // replace it with the original one, that we got from the response
34 return replace(prev, foundIndex, res.message);
35 });
36})
37.catch(() => {
38 setMessages((prev) => {
39 // find the index of the fake added message
40 const foundIndex = prev.findIndex((item) => item.id == id);
41
42 // replace it with the another one with status='ERROR'
43 return replace(prev, foundIndex, draftNewMessage('ERROR'));
44 });
45})}
46...

Now let’s turn the network off, and try to send a message.

Error messages

Share it mate 😎.

More articles from Obytes

Strapi Getting Started with Gatsby

We all know that a headless CMS can deliver your content through an API directly to where you need it. Because of the headless approach the…

March 17th, 2021 · 2 min read

How we used Cloudflare Argo Tunnels + Access to replace a VPN

An alternative to a VPN provided by Cloudflare, and how we set it up on AWS using Terraform

March 15th, 2021 · 4 min read

ABOUT US

Our mission and ambition is to challenge the status quo, by doing things differently we nurture our love for craft and technology allowing us to create the unexpected.