react: Bug: element.current.scrollIntoView() not working as expected in chrome.

Hey 🙋‍♂️

I have built a simple chatbot using react, and when the user sends a message i want to show the last message in the chatWindow component, so I used the following code:

useEffect(
    function () {
      if (lastmessageRef.current !== null) {
        lastmessageRef.current.scrollIntoView()
      }
    },
    [lastmessageRef]
  )

It works as expected in edge and Firefox, but on chrome it is behaving weird llink-to-the-chatbot: https://karthik2265.github.io/worlds-best-chatbot/ github-repo-link: https://github.com/karthik2265/worlds-best-chatbot

Thank you

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Reactions: 14
  • Comments: 22

Commits related to this issue

Most upvoted comments

A workaround I’ve found it to use window.requestAnimationFrame:

useEffect(() => {
  if (itemsUpdated) {
    window.requestAnimationFrame(() => 
      elRef.current?.scrollIntoView()
    )
  }
}, [])

I had the same problem So I solved this by using setTimeout. (The flicker looks weird, but it works.)

// move to todo post
  useEffect(() => {
      ...
      const timer = setTimeout(() => {
      const todoPost = document.querySelector(`[data-post="${selectedTodoPost}"]`); 

      if (!todoPost) {
        return;
      }

      todoPost.scrollIntoView({ block: 'center' });
    }, 500);

    return () => clearTimeout(timer);
  }, [...]);

Just wanted to shed some light into this issue and why it can probably be closed.

This has nothing to do with react. This is a problem that is related to the chromium implementation of the scrollIntoView method. There’s been some open issues for a long time in the chromium project regarding this issue.

https://bugs.chromium.org/p/chromium/issues/detail?id=1121151 https://bugs.chromium.org/p/chromium/issues/detail?id=1043933 https://bugs.chromium.org/p/chromium/issues/detail?id=833617

So from what I’ve got from all the testing and searching the last few days is that chromium based browsers interrupt the scrollIntoView when any other scroll event is triggered.

This only happens with the scrollIntoView method, the scrollTo and scroll methods don’t suffer from this issue. You can see and test this by yourself on the following fiddle.

https://jsfiddle.net/2bnspw8e/8/

So then what’s a solution if you want to keep the simplicity of scrollIntoView and have other scroll happen at the same time?

There is a ponyfill version of this method which reimplements it using the non broken scroll method.

I hope this clarifies what’s actually happening behind the scene and provides a solution while we all wait for a chromium fix.

For me it works if I remove the behavior: 'smooth', prop, however, I really want the smooth scrolling behaviour 😦

IMO almost all the answers above are not correct.

You are trying to invoke scrolling in Effect but it should be done in LayoutEffect.

Function inside effect executes in parallel to DOM update commits and in 99% it will be invoked faster. So normally real DOM (according to the vDOM built up with your render function) is not ready yet at that point of time. This is why “workarounds” using setTimeout work somehow. But using LayoutEffect is more accurate.

I’m having the same problem. scrollIntoView api doesn’t work inside useEffect, while it fires by onClick. Worth mentioning that it works in Firefox. Here is the code:

import React, { useEffect, useRef, useState } from 'react'
import FirstPage from './components/FirstPage'
import SecondPage from './components/SecondPage'
import ThirdPage from './components/ThirdPage'

import './App.css'

const App = () => {
  const [atPage, setAtPage] = useState<number>(0)
  const refs = useRef<(null | HTMLDivElement)[]>([])
  const pages = [<FirstPage />, <SecondPage />, <ThirdPage />]

  const handleWheel = (event: any) => {
    console.log(event.deltaY > 0 ? 'down' : 'up')
    setAtPage(atPage => {
      let atPageCopied = JSON.parse(JSON.stringify(atPage))
      return event.deltaY < 0 ? (atPage > 0 ? atPageCopied - 1 : atPage) : (atPage < pages.length - 1 ? atPageCopied + 1 : atPage)
    })
  }
  const scrollItThere = (atPage: number) => refs.current[atPage]?.scrollIntoView({ block: 'start', behavior: 'smooth' })

  useEffect(() => {
    console.log(atPage)
    scrollItThere(atPage)
  }, [atPage])


  return (
    <div onWheel={handleWheel} >
      {pages.map((el, index) => <div
        key={index}
        ref={i => refs.current[index] = i}
        onClick={() => refs.current[0]?.scrollIntoView({ block: 'start', behavior: 'smooth' })}
      >
        {el}
      </div>)}
    </div>
  )
}

export default App

@hwangyena cool little thing I found out – you can do what you did setTimeout(..., 0) and it works as well without delay, at least in my use case.

It’s basically just a trick to defer until next tick in JS.

I had the same problem and managed to fix it by using the scroll method for the parent node instead of scrollIntoView for the children node:

const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
        const scrollToTheBottom = () => {
            const scrollEl = ref.current;
            scrollEl?.scroll({
                top: scrollEl?.scrollHeight,
                behavior: 'smooth',
            });
        };
        scrollToTheBottom();
}, []);

return (
        <Wrap>
            <Scroll ref={ref}>
                <MessageGroup messages={messagesIncome} />
                <MessageGroup messages={messagesIncome} />
                <MessageGroup messages={messagesIncome} />
                <MessageGroup messages={messagesIncome} />
                <MessageGroup messages={messagesIncome} />
                <MessageGroup messages={messagesIncome} />
                <MessageGroup messages={messagesIncome} />
                <MessageGroup messages={messagesIncome} />
                <MessageGroup messages={messagesIncome} />
                <MessageGroup messages={messagesIncome} />
                <MessageGroup messages={messagesIncome} />
                <MessageGroup messages={messagesIncome} />
                <MessageGroup messages={messagesIncome} />
                <MessageGroup messages={messagesIncome} />
                <MessageGroup messages={messagesIncome} />
                <MessageGroup messages={messagesIncome} />
                <MessageGroup messages={messagesIncome} />
            </Scroll>
        </Wrap>
);

Works fine in Firefox, Chrome, Safari, Edge

https://user-images.githubusercontent.com/26645888/217844587-0b5d64d7-f34d-4128-8e43-9b7cefdc9403.mp4

@pjm4 Yeah, that was the case for me as well.

This is what I did as a workaround:

const [shouldScrollToRow, setShouldScrollToRow] = useState(false);

    useEffect(() => {
        if (shouldScrollToRow) {
            setTimeout(() => {
                elemRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' });
            }, 100);
            setShouldScrollToRow(false);
        }
    }, [shouldScrollToRow]);

    useEffect(() => {
        // Listening for items getting added
        if (itemsUpdated) {
            setShouldScrollToRow(true);
            setItemsUpdated(false);
        }
    // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [itemsUpdated]);

I want to know why this is happening

Note that lastmessageRef as a dependency won’t do anything and can be removed. The effect won’t fire if lastmessageRef.current changes.

Could you include a video of the behavior in Firefox and Chrome and explain with words what’s “weird” about this behavior? Also please include the exact version of those browsers and the operating system you’re using.

Bonus: Could you convert the repro into a codesandbox.io? Makes it easier to debug the issue.

Thanks for the digging and, I always wanted a one-trick pony!

This shows up at just the right time!

Cheers!