Why isn't scrollIntoView working within Vue watcher to automatically scroll to new messages in chat app?

38 Views Asked by At

I am learning how to build chat apps in Vue 3. One thing I am confused about is why the app example I made does not automatically scroll to the bottom of the screen when a new message appears (a common experience in chat apps).

Anyone know why when a new message is loaded, the browser does not automatically scroll the user down to the bottom of the screen despite my scrollIntoView method? It "works" when I manually click the "Scroll" button at the top of the page, but for some reason the Watcher is not able to do it.

Here's my code: Vue SFC playground

<template>
  <section>
    <button @click="scrollToElement()">Scroll</button>
    <ul ref="chatContainer">
      <li v-for="message in messages" :key="message.id">
        {{ message.title }}
      </li>
    </ul>
  </section>
</template>

<script setup>
import { onMounted, ref, watch } from 'vue'

const chatContainer = ref(null)
const messages = ref([])

const fetchChatMessages = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts')
  const messagesArr = await response.json()
  messages.value = messagesArr
}

const addNewMessage = () => {
  // Simulate a new message and add it to the chat
  const newMessage = {
    userId: 1,
    id: messages.value.length + 1,
    title: 'New Message',
    body: 'This is a new chat message.'
  }
  messages.value.push(newMessage)
}

const scrollToElement = () => {
  const el = chatContainer.value
  el.scrollIntoView({ block: 'end', behavior: 'smooth' })
}

watch(messages, () => {
  if (chatContainer.value) {
    scrollToElement()
  }
}, {
  immediate: true
})

onMounted(async () => {
  await fetchChatMessages()

  setInterval(() => {
    addNewMessage()
  }, 1000)
})
</script>
2

There are 2 best solutions below

1
Alexander Nenashev On BEST ANSWER

messages is a ref and unfortunately a ref doesn't deep watch objects. That's actually misleading, I even created an issue: https://github.com/vuejs/docs/issues/2454#issuecomment-1783779632

But you don't need to deep watch your messages since it's expensive, just use a callback watch:

watch(() => messages.value.length, () => {
  if (chatContainer.value) {
    scrollToElement()
  }
}, {
  immediate: true
})

A more detailed explanation of the problem is here:

https://stackoverflow.com/a/76786671/14098260

0
Wesley Cheek On

You need a deep watcher for watching nested objects and arrays:

watch(messages, () => {
  if (chatContainer.value) {
    scrollToElement()
  }
}, {
  immediate: true,
  deep: true
})