facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
227.91k stars 46.51k forks source link

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

Open karthik2265 opened 2 years ago

karthik2265 commented 2 years ago

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

eps1lon commented 2 years ago

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.

noviceGuru commented 2 years ago

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
vigneshvjay commented 2 years ago

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

This works like a charm. Thanks buddy

ValenW commented 1 year ago

Come accross this issue with

React: 18.2.0
Chrome: Version 108.0.5359.125 (Official Build) (64-bit)
FireFox Developer Edition 109.0b9

, that element.current.scrollIntoView works in FF but not in Chrome, is there any progress?

hwangyena commented 1 year ago

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);
  }, [...]);
jalexy12 commented 1 year ago

@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.

eugenegodun commented 1 year ago

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

danielo515 commented 1 year ago

The problem doesn't seem to be limited to lastmessageRef.current if you fetch the component by ID, it also does not work on chrome, but works fine in firefox.

ajoslin103 commented 1 year ago

Not working in Electron 19 (Chromium)

This was working in React 16, I've since updated the project to React 18

pjm4 commented 1 year ago

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

subhamayd2 commented 1 year ago

@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]);
Jerry-qiqi commented 1 year ago

I want to know why this is happening

jordanl17 commented 1 year ago

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

useEffect(() => {
  if (itemsUpdated) {
    window.requestAnimationFrame(() => 
      elRef.current?.scrollIntoView()
    )
  }
}, [])
mateussilva92 commented 1 year ago

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.

ajoslin103 commented 1 year ago

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

This shows up at just the right time!

Cheers!

bn1ck commented 1 year ago

I ended up creating my own helper function.

export const smoothScroll = (targetY: number, duration: number) => {
  const startingY = window.pageYOffset;
  const diff = targetY - startingY;
  let start: number;

  window.requestAnimationFrame(function step(timestamp) {
    if (!start) start = timestamp;
    const time = timestamp - start;
    const percent = Math.min(time / duration, 1);

    window.scrollTo(0, startingY + diff * percent);

    if (time < duration) {
      window.requestAnimationFrame(step);
    }
  });
}

To call the function i use the following

if (viewerRef.current) {
      const top = viewerRef.current.offsetTop;
      smoothScroll(top, 500);
}
iuriikomarov commented 9 months ago

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.

github-actions[bot] commented 5 months ago

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

danielo515 commented 5 months ago

this is not stale, because it is not fixed

hypo-thesis commented 5 months ago

This is not fixed for sure. behavior: 'smooth' does work on FF but not on Chrome. I have codes to reproduce if you wish.

missalyss commented 5 months ago

I want to add that the useRef, useEffect combo didn't work for me (no movement at all, because elementRef.current wasn't defined until after the effect had run). Instead, I updated to useCallback.

  const elementRef = useCallback((domEl) => {
    if (domEl !== null ) {
      domEl.scrollIntoView({
        behavior: 'smooth',
      });
    }
  }, []);

...

<div ref={elementRef} >...</div>
alexpeev9 commented 5 months ago

Hello.

After checking different scenarios, I also concluded that there is an issue with the way Chrome behaves with scrollIntoView() when you set the behavior to smooth.

In my case, the scroll randomly stops at the start or middle of the scroll. A temporary solution for me would be to disable the smooth scroll for Chrome browsers.

const behavior = navigator.userAgent.includes('Chrome') ? 'auto' : 'smooth'
ref.current?.scrollIntoView({ behavior: behavior, block: 'start' })
Qnemes commented 3 months ago

In my case I`ve encountered weird behavior with simple onPress event with window.scrollTo({ top: scrollRef.current.offsetTop, behavior: 'smooth' }); So I've wrapped this call with setTimeout 0ms and it fixed unexpected behavior (stops mid scroll) completely.

subzeta commented 3 months ago

Hello.

After checking different scenarios, I also concluded that there is an issue with the way Chrome behaves with scrollIntoView() when you set the behavior to smooth.

In my case, the scroll randomly stops at the start or middle of the scroll. A temporary solution for me would be to disable the smooth scroll for Chrome browsers.

const behavior = navigator.userAgent.includes('Chrome') ? 'auto' : 'smooth'
ref.current?.scrollIntoView({ behavior: behavior, block: 'start' })

This was the issue in my case. Thanks mate!

satejbidvai commented 3 weeks ago

Any updates on this?

I still seem to be getting this exact issue on Chrome. Removing behaviour: smooth works.

mmis1000 commented 3 weeks ago

Any updates on this?

I still seem to be getting this exact issue on Chrome. Removing behaviour: smooth works.

You should probably look into related chrome issue instead. This is a chrome bug that has nothing to do with react.

https://issues.chromium.org/issues/325081538

The recent comment says they are changing how smooth in to view smooth work in chrome, which may fix this issue.

scath999 commented 1 week ago

Still not working for me in either Chrome or Firefox on desktop.

I enclose an excerpt from some messaging functionality - users can open and close a chat window from a list of available conversations.

The first time I open the window, the content appears but the window doesn't scroll. If I then close and reopen the window it works as intended, as does posting new text to the window.

Note: The placement of scrollIntoView() is in two different places within these methods - this is intentionally done to test the behaviour; afaik both should work, neither does.

     const scroll_position = useRef()

     (...) 

     function GetMessageThread(thread_id){

        axios.post("/api/get_message_thread", {
            headers: {
                Accept: 'application/json',
                'Content-Type': 'application/json',
                withCredentials: true
            },
            body: JSON.stringify({"thread_id" : thread_id})
        })
        .then((response) => {
            if (response.statusText == "OK"){
                setMessageList(response.data['message_list'])
                console.log(":: GetMessageThread :: Scrolling to bottom of window ")
                console.log("-- scroll_position.current =", scroll_position.current)
                scroll_position.current.scrollIntoView()

            }
        })
    }

    function NewMessage(e){
        e.preventDefault();

        var payload = {"message" : newMessage, "thread_id" : thread_id}
        axios.post('/api/new_message', {
            headers: {
                Accept: 'application/json',
                'Content-Type': 'application/json',
                withCredentials: true
            },
            body: JSON.stringify(payload)
        }).then((response) =>{
            if (response.statusText == "OK"){
               console.log("response from new_message = OK")

            }
        })

        setNewMessage('')
        console.log(":: NewMessage :: Scrolling to bottom of window")
        scroll_position.current.scrollIntoView({ behavior : 'smooth'})

    }

    return(
       <>
        (...)
         <DialogContent>            
                {messageList.map((msg, i) => 
                    {
                        return (<ChatMessage key={i} messageObj={msg}  />)
                    }
                )}
                <span id="scroll_pos" name="scroll_pos" ref={scroll_position}></span>
            </DialogContent>
        (...)
        </>
    )

Output in console, whether on first or subsequent opens, is as follows :

:: GetMessageThread :: Scrolling to bottom of window 
-- scroll_position.current = <span id=​"scroll_pos" name=​"scroll_pos">​</span>​