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 */)
...
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 */)}
...
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.
Share it mate 😎.