Handle and track chat messages delivery using React

Handle and track chat messages delivery using React

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

...
const { createMessage } = useMutation(CREATE_MESSAGE);

const handleSendMessage = (message: string) =>
 createMessage({ variables: { message } })
 .then(res => /** use the res to update the old messages array */)
 .catch(() => /** throw an error */)
...

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.

...
const [messages, setMessages] = useState([])
const { createMessage } = useMutation(CREATE_MESSAGE);

const handleSendMessage = (message:string) =>
createMessage({ variables: { message }})
  .then(res => {
    setMessages(prev => [...prev, res.message]);
    resetForm();
  })
  .catch(() => /** throw an error */)
...
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.

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

Usage

generateUniqueId('newMessage_');
// 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.

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

Usage

const array = ['JS', 'JAVA', 'PYTHON'];
replace(array, 1, 'TS');
// 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.

{
 id : string;
 sender: string;
 body: string;
 createdAt: date;
 status: 'PENDING' | 'SENT' | 'ERROR'; // The new added property
}

Implementation

Utils

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

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

SendMessageInput component

...
const currentUser = useCurrentUser()
const [messages, setMessages] = useState([])
const { createMessage } = useMutation(CREATE_MESSAGE);

const handleSendMessage = (message: string) => {
// new message with fake temporary id
const draftNewMessage = {
 id : generateUniqueId('newMessage_'),
 sender: currentUser.id,
 body: message,
 createdAt: new Date(),
 status: 'PENDING'
}

// add the new message once the user hits send
setMessages(prev => [...prev, draftNewMessage]);

// clear the form input once the user hits send
resetForm();

// send the message to the backend
createMessage({ variables: { message } })
.then(res => {
  setMessages((prev) => {
    // find the index of the fake added message
    const foundIndex = prev.findIndex((item) => item.id == id);

    // replace it with the original one, that we got from the response
    return replace(prev, foundIndex, res.message);
  });
})
.catch(() => /** throw an error */)}
...
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.

const draftNewMessage = (status) => ({
 id : generateUniqueId('newMessage_'),
 sender: currentUser.id,
 body: message,
 createdAt: new Date(),
 status
})

Let's have a look into it now:

...
const currentUser = useCurrentUser()
const [messages, setMessages] = useState([])
const { createMessage } = useMutation(CREATE_MESSAGE);

const handleSendMessage = (message: string) => {

// generating the new ID
const id = generateUniqueId('_new_message__');

// new message with fake temporary id
const draftNewMessage = (status) => ({
 id,
 sender: currentUser.id,
 body: message,
 createdAt: new Date(),
 status
})

// add the new message once the user hits send
setMessages(prev => [...prev, draftNewMessage('PENDING')]);

// clear the form input once the user hits send
resetForm();

// send the message to the backend
createMessage({variables: { message }})
.then(res => {
  setMessages((prev) => {
    // find the index of the fake added message
    const foundIndex = prev.findIndex((item) => item.id == id);

    // replace it with the original one, that we got from the response
    return replace(prev, foundIndex, res.message);
  });
})
.catch(() => {
  setMessages((prev) => {
    // find the index of the fake added message
    const foundIndex = prev.findIndex((item) => item.id == id);

    // replace it with the another one with status='ERROR'
    return replace(prev, foundIndex, draftNewMessage('ERROR'));
  });
})}
...

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

Error messages

Share it mate 😎.

Ilyass Ben Hakim
Ilyass Ben Hakim
2021-09-02 | 6 min read
Share article

More articles