I wrote a component called Chat (some parts are omitted for simplicity):
function Chat() {
const containerRef = React.useRef(null);
const messages = useQuery({
queryKey: ["messages"],
queryFn: api.getMessages
});
const createMessage = useMutation({
mutationFn: (message) => api.createMessage(message),
onMutate: (message) => {
// 1. cancels query, updates cache, returns previous value
optimisitcUpdate(messages, message);
// 2. scroll to bottom
const { current: container } = containerRef;
// ❌ won't work, because the DOM is not updated yet
container?.scrollTo(0, container.scrollHeight);
// 💩 works, because the DOM is updated, but it's hacky
setTimeout(() => container?.scrollTo(0, container.scrollHeight))
},
});
return (
<div id="messages" ref={containerRef}>
{messages.data?.map((message) => (
<div key={message.uuid}>{message.body}</div>
))}
</div>
);
}
It’s pretty straightforward. useQuery to fetch all messages, useMutation to create a new message, and return all of the messages.
Whenever createMessage occurs, I want to optimistically update the messages and then scroll to the bottom (like any other chat behavior).
The problem is, I have no way of doing flushSync inside useMutation, so as a workaround, I wrap the logic inside a setTimeout, but that’s a hacky way.
I was wondering if there’s a more robust approach than this.
>Solution :
You can rely on useEffect to execute after the DOM rendering:
useEffect(() => {
const { current: container } = containerRef;
container?.scrollTo(0, container.scrollHeight);
)/*, [you may want to put some dependencies here to limit rendering]*/};