I have a simple chat component, as a part of a bigger app.
- After logging in, userMessages are fetched from the backend and stored in useState.
const [userMessages, setUserMessages] = useState<ChatMessage[]>([]);
const [messages, setMessages] = useState<ChatMessage[]>([]);
userMessages - all the messages addressed to the user (from other users). Based on them, unread messages are displayed.
messages - messages belonging to a given conversation (between two users) are fetched when entering a given conversation, appear in the chat window.
- When a user gets a new message while not being on chat, he gets notifications about unread messages (I used socket.io).
- After clicking on the blue arrow icon, the current conversation is set (based on the message property - currentConversationId) and the messages belonging to this conversation are fetched from the database.
When they appear in the chat window each received message (only the green ones) is read...
...each message.tsx component has an useEffect that sends a request to the backend to change the status of a given message from unread to read and returns this message to the frontend, then the messages are updated using useState).
# message.tsx
useEffect(() => {
!own && !read && onReadMessage?.(_id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
# communicationContext.tsx
const onReadMessage = async (id: string | undefined) => {
const updatedMessage = await CommunicationService.readMessage(id);
if (updatedMessage) {
let notification = {
receiverId: updatedMessage.sender?._id,
text: "read your message.",
silent: true,
read: false,
type: "message",
};
handleAddNotification?.(notification);
handleSendNotification?.({
...notification,
senderId: updatedMessage.sender?._id,
senderName: updatedMessage.sender?.name,
payload: { message: updatedMessage },
});
const updatedMessages = messages.map((message) =>
message._id === updatedMessage._id ? updatedMessage : message
);
setMessages(updatedMessages);
const updatedUserMessages = userMessages.map((message) =>
message._id === updatedMessage._id ? updatedMessage : message
);
setUserMessages(updatedUserMessages);
}
}
- A request containing an updated message is also sent to the sender of the message via socket.io, then useState is also fired on the sender side and they see that the message has been read.
Up to this point everything works fine, but...
the problem arises when there are several unread messages at the same time.
In the database all messages are updated but the application status shows only 1-3 latest messages as read (depending on how many there are - sometimes only the last one is updated).
I know how useState works, so I expected this result, but I'm looking for a way around it and I'm out of ideas.
I need a solution that will update the entire state of the application, not just recent changes, without having to refresh the page.
I tried useReducer but got lost because there are too many useStates in communicationContext.tsx (here is a simplified version).
CodePudding user response:
I suspect your onReadMessage
handler should use functional state updates to eliminate race conditions or updating from stale state. It's a trivial change. The enqueued functional state updates will correctly update from the previous state versus whatever (likely stale) state value(s) are closed over in callback scope.
const onReadMessage = async (id: string | undefined) => {
const updatedMessage = await CommunicationService.readMessage(id);
if (updatedMessage) {
const notification = {
receiverId: updatedMessage.sender?._id,
text: "read your message.",
silent: true,
read: false,
type: "message",
};
handleAddNotification?.(notification);
handleSendNotification?.({
...notification,
senderId: updatedMessage.sender?._id,
senderName: updatedMessage.sender?.name,
payload: { message: updatedMessage },
});
const mapMessage = (message) => message._id === updatedMessage._id
? updatedMessage
: message;
setMessages(messages => messages.map(mapMessage));
setUserMessages(userMessages => userMessages.map(mapMessage));
}
};