【问题标题】:How to scroll to bottom in react?如何在反应中滚动到底部?
【发布时间】:2016-10-03 21:25:47
【问题描述】:

我想搭建一个聊天系统,在进入窗口和有新消息进来时自动滚动到底部。React中如何自动滚动到容器底部?

【问题讨论】:

    标签: reactjs


    【解决方案1】:

    您可以使用refs 来跟踪组件。

    如果您知道设置单个组件(最后一个)的ref 的方法,请发布!

    以下是我发现对我有用的方法:

    class ChatContainer extends React.Component {
      render() {
        const {
          messages
        } = this.props;
    
        var messageBubbles = messages.map((message, idx) => (
          <MessageBubble
            key={message.id}
            message={message.body}
            ref={(ref) => this['_div' + idx] = ref}
          />
        ));
    
        return (
          <div>
            {messageBubbles}
          </div>
        );
      }
    
      componentDidMount() {
        this.handleResize();
    
        // Scroll to the bottom on initialization
        var len = this.props.messages.length - 1;
        const node = ReactDOM.findDOMNode(this['_div' + len]);
        if (node) {
          node.scrollIntoView();
        }
      }
    
      componentDidUpdate() {
        // Scroll as new elements come along
        var len = this.props.messages.length - 1;
        const node = ReactDOM.findDOMNode(this['_div' + len]);
        if (node) {
          node.scrollIntoView();
        }
      }
    }
    

    【讨论】:

      【解决方案2】:

      我在消息末尾创建了一个空元素,然后滚动到该元素。无需跟踪 refs。

      【讨论】:

      • 你是怎么做到的
      • @mmla 您在使用 Safari 时遇到了什么问题?它不能可靠地滚动吗?
      【解决方案3】:

      正如 Tushar 所说,您可以在聊天底部保留一个虚拟 div:

      render () {
        return (
          <div>
            <div className="MessageContainer" >
              <div className="MessagesList">
                {this.renderMessages()}
              </div>
              <div style={{ float:"left", clear: "both" }}
                   ref={(el) => { this.messagesEnd = el; }}>
              </div>
            </div>
          </div>
        );
      }
      

      然后在您的组件更新时滚动到它(即状态随着新消息的添加而更新):

      scrollToBottom = () => {
        this.messagesEnd.scrollIntoView({ behavior: "smooth" });
      }
      
      componentDidMount() {
        this.scrollToBottom();
      }
      
      componentDidUpdate() {
        this.scrollToBottom();
      }
      

      我在这里使用标准的Element.scrollIntoView 方法。

      【讨论】:

      • 来自文档的警告:“findDOMNode 不能用于功能组件。”
      • this.messagesEnd.scrollIntoView() 对我来说效果很好。没有必要使用findDOMNode()
      • 即使您向上滚动,它也会滚动到底部,这会破坏您的 UI 体验。在某些情况下,您需要一个标志来忽略滚动到底部
      • 好的,我删除了 findDOMNode。如果这对某人不起作用,您可以查看答案的编辑历史记录。
      • 我有一个错误,scrollIntoView 是 TypeError:无法读取未定义的属性“scrollIntoView”。怎么办?
      【解决方案4】:

      感谢@enlitement

      我们应该避免使用findDOMNode, 我们可以使用refs 来跟踪组件

      render() {
        ...
      
        return (
          <div>
            <div
              className="MessageList"
              ref={(div) => {
                this.messageList = div;
              }}
            >
              { messageListContent }
            </div>
          </div>
        );
      }
      
      
      
      scrollToBottom() {
        const scrollHeight = this.messageList.scrollHeight;
        const height = this.messageList.clientHeight;
        const maxScrollTop = scrollHeight - height;
        this.messageList.scrollTop = maxScrollTop > 0 ? maxScrollTop : 0;
      }
      
      componentDidUpdate() {
        this.scrollToBottom();
      }
      

      参考:

      【讨论】:

      • 我觉得这个解决方案最合适,因为它不会向 DOM 添加新的(虚拟)元素,而是直接处理现有的,感谢 jk2k
      • 像魅力一样工作!荣誉
      【解决方案5】:
      1. 引用您的消息容器。

        <div ref={(el) => { this.messagesContainer = el; }}> YOUR MESSAGES </div>
        
      2. 找到您的消息容器并使其scrollTop 属性等于scrollHeight

        scrollToBottom = () => {
            const messagesContainer = ReactDOM.findDOMNode(this.messagesContainer);
            messagesContainer.scrollTop = messagesContainer.scrollHeight;
        };
        
      3. componentDidMountcomponentDidUpdate 上调用上述方法。

        componentDidMount() {
             this.scrollToBottom();
        }
        
        componentDidUpdate() {
             this.scrollToBottom();
        }
        

      这就是我在代码中使用它的方式:

       export default class StoryView extends Component {
      
          constructor(props) {
              super(props);
              this.scrollToBottom = this.scrollToBottom.bind(this);
          }
      
          scrollToBottom = () => {
              const messagesContainer = ReactDOM.findDOMNode(this.messagesContainer);
              messagesContainer.scrollTop = messagesContainer.scrollHeight;
          };
      
          componentDidMount() {
              this.scrollToBottom();
          }
      
          componentDidUpdate() {
              this.scrollToBottom();
          }
      
          render() {
              return (
                  <div>
                      <Grid className="storyView">
                          <Row>
                              <div className="codeView">
                                  <Col md={8} mdOffset={2}>
                                      <div ref={(el) => { this.messagesContainer = el; }} 
                                           className="chat">
                                          {
                                              this.props.messages.map(function (message, i) {
                                                  return (
                                                      <div key={i}>
                                                          <div className="bubble" >
                                                              {message.body}
                                                          </div>
                                                      </div>
                                                  );
                                              }, this)
                                          }
                                      </div>
                                  </Col>
                              </div>
                          </Row>
                      </Grid>
                  </div>
              );
          }
      }
      

      【讨论】:

        【解决方案6】:

        作为另一种选择,react scroll 组件值得一看。

        【讨论】:

          【解决方案7】:

          我喜欢这样做。

          componentDidUpdate(prevProps, prevState){
            this.scrollToBottom();
          }
          
          scrollToBottom() {
            const {thing} = this.refs;
            thing.scrollTop = thing.scrollHeight - thing.clientHeight;
          }
          
          render(){
            return(
              <div ref={`thing`}>
                <ManyThings things={}>
              </div>
            )
          }
          

          【讨论】:

            【解决方案8】:

            不要使用findDOMNode

            带有 ref 的类组件

            class MyComponent extends Component {
              componentDidMount() {
                this.scrollToBottom();
              }
            
              componentDidUpdate() {
                this.scrollToBottom();
              }
            
              scrollToBottom() {
                this.el.scrollIntoView({ behavior: 'smooth' });
              }
            
              render() {
                return <div ref={el => { this.el = el; }} />
              }
            }
            

            带有钩子的函数组件:

            import React, { useRef, useEffect } from 'react';
            
            const MyComponent = () => {
              const divRref = useRef(null);
            
              useEffect(() => {
                divRef.current.scrollIntoView({ behavior: 'smooth' });
              });
            
              return <div ref={divRef} />;
            }
            

            【讨论】:

            • 你能解释一下为什么不应该使用 findDOMNode 吗?
            • @steviekins 因为“它阻碍了 React 的某些改进”并且可能会被弃用 github.com/yannickcr/eslint-plugin-react/issues/…
            • 应该是behavior 的美式拼写(不能编辑,因为“编辑必须至少有 6 个字符”,叹息)。
            • 目前对scrollIntoViewsmooth 的支持很差。
            • 对我很有帮助的帖子!
            【解决方案9】:

            工作示例:

            您可以使用 DOM scrollIntoView 方法使组件在视图中可见。

            为此,在渲染组件时,只需使用 ref 属性为 DOM 元素提供参考 ID。然后在componentDidMount生命周期上使用scrollIntoView的方法。我只是为这个解决方案提供一个工作示例代码。以下是每次收到消息时的组件渲染。你应该编写代码/方法来渲染这个组件。

            class ChatMessage extends Component {
                scrollToBottom = (ref) => {
                    this.refs[ref].scrollIntoView({ behavior: "smooth" });
                }
            
                componentDidMount() {
                    this.scrollToBottom(this.props.message.MessageId);
                }
            
                render() {
                    return(
                        <div ref={this.props.message.MessageId}>
                            <div>Message content here...</div>
                        </div>
                    );
                }
            }
            

            这里this.props.message.MessageId 是作为props 传递的特定聊天消息的唯一ID

            【讨论】:

            • 令人惊叹的 Sherin bhai 它就像蛋糕一样工作。谢谢
            • @MohammedSarfaraz 很高兴我能帮上忙 :)
            【解决方案10】:
            import React, {Component} from 'react';
            
            export default class ChatOutPut extends Component {
            
                constructor(props) {
                    super(props);
                    this.state = {
                        messages: props.chatmessages
                    };
                }
                componentDidUpdate = (previousProps, previousState) => {
                    if (this.refs.chatoutput != null) {
                        this.refs.chatoutput.scrollTop = this.refs.chatoutput.scrollHeight;
                    }
                }
                renderMessage(data) {
                    return (
                        <div key={data.key}>
                            {data.message}
                        </div>
                    );
                }
                render() {
                    return (
                        <div ref='chatoutput' className={classes.chatoutputcontainer}>
                            {this.state.messages.map(this.renderMessage, this)}
                        </div>
                    );
                }
            }
            

            【讨论】:

              【解决方案11】:

              我只是想更新答案以匹配新的React.createRef()method,但基本相同,只需记住创建的参考中的current 属性:

              class Messages extends React.Component {
              
                const messagesEndRef = React.createRef()
              
                componentDidMount () {
                  this.scrollToBottom()
                }
                componentDidUpdate () {
                  this.scrollToBottom()
                }
                scrollToBottom = () => {
                  this.messagesEndRef.current.scrollIntoView({ behavior: 'smooth' })
                }
                render () {
                  const { messages } = this.props
                  return (
                    <div>
                      {messages.map(message => <Message key={message.id} {...message} />)}
                      <div ref={this.messagesEndRef} />
                    </div>
                  )
                }
              }
              

              更新:

              现在钩子可用,我正在更新答案以添加 useRefuseEffect 钩子的使用,真正发挥作用的东西(React refs 和 scrollIntoView DOM 方法)保持不变:

              import React, { useEffect, useRef } from 'react'
              
              const Messages = ({ messages }) => {
              
                const messagesEndRef = useRef(null)
              
                const scrollToBottom = () => {
                  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
                }
              
                useEffect(() => {
                  scrollToBottom()
                }, [messages]);
              
                return (
                  <div>
                    {messages.map(message => <Message key={message.id} {...message} />)}
                    <div ref={messagesEndRef} />
                  </div>
                )
              }
              

              如果您想检查行为https://codesandbox.io/s/scrolltobottomexample-f90lz,还制作了一个(非常基本的)代码框

              【讨论】:

              • componentDidUpdate 可以在 React 生命周期中多次调用。因此,我们应该检查 ref this.messagesEnd.current 是否存在于 scrollToBottom 函数中。如果 this.messagesEnd.current 不存在,则错误消息将显示 TypeError: Cannot read property 'scrollIntoView' of null。所以,添加这个 if 条件也 scrollToBottom = () => { if (this.messagesEnd.current) { this.messagesEnd.current.scrollIntoView({ behavior: 'smooth' }) } }
              • 第二个示例代码不起作用。 useEffect方法需要和() =&gt; {scrollToBottom()}放在一起。无论如何都非常感谢
              • 我对@9​​87654332@ 的这个实现有一个错误:“当前”是null,直到渲染。为了解决这个问题,我将if (messagesEndRef.current) 放在`scrollToBottom 函数中。
              • 修复打字稿错误“打字稿错误:“从不”类型上不存在属性“scrollIntoView”。 TS2339' -> 使用 useRef 分配正确的类型: const scrollRef = useRef(null)
              • @DiegoLara 第一个示例(类组件)有错字,您输入的 this.messagesEnd.current 是错误的,应该是 this.messagesEndRef.current。
              【解决方案12】:

              react-scrollable-feed 如果用户已经在可滚动部分的底部,则自动向下滚动到最新的元素。否则,它会将用户留在相同的位置。我认为这对聊天组件非常有用:)

              我认为这里的其他答案每次都会强制滚动,无论滚动条在哪里。 scrollIntoView 的另一个问题是,如果您的可滚动 div 不在视图中,它将滚动整个页面。

              可以这样使用:

              import * as React from 'react'
              
              import ScrollableFeed from 'react-scrollable-feed'
              
              class App extends React.Component {
                render() {
                  const messages = ['Item 1', 'Item 2'];
              
                  return (
                    <ScrollableFeed>
                      {messages.map((message, i) => <div key={i}>{message}</div>)}
                    </ScrollableFeed>
                  );
                }
              }
              

              只需确保有一个带有特定heightmax-height 的包装器组件

              免责声明:我是包的所有者

              【讨论】:

              • 谢谢,我使用了你的控件。注意:我必须使用 forceScroll=true 让它按需要工作,由于某种原因,当滚动条开始出现时它没有自动滚动到顶部。
              • @Patric 如果您可以open an issue on GitHub 提供有关您的设置的一些详细信息,也许我们可以找出问题所在?
              • 这正是我想要的,控件只在需要时滚动,ScrollIntoView 只是滚动一点,而不是滚动到底部。
              【解决方案13】:

              完整版(打字稿):

              import * as React from 'react'
              
              export class DivWithScrollHere extends React.Component<any, any> {
              
                loading:any = React.createRef();
              
                componentDidMount() {
                  this.loading.scrollIntoView(false);
                }
              
                render() {
              
                  return (
                    <div ref={e => { this.loading = e; }}> <LoadingTile /> </div>
                  )
                }
              }

              【讨论】:

              • 这给了我各种错误:Property 'scrollIntoView' does not exist on type 'RefObject&lt;unknown&gt;'.Type 'HTMLDivElement | null' is not assignable to type 'RefObject&lt;unknown&gt;'. Type 'null' is not assignable to type 'RefObject&lt;unknown&gt;'. 所以...
              • 请问 ReactJS 的版本?我正在使用 1.16.0
              【解决方案14】:

              感谢 'metakermit' 的好回答,但我认为我们可以做得更好一点, 对于滚动到底部,我们应该使用这个:

              scrollToBottom = () => {
                 this.messagesEnd.scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" });
              }
              

              但如果你想滚动顶部,你应该使用这个:

              scrollToTop = () => {
                 this.messagesEnd.scrollIntoView({ behavior: "smooth", block: "start", inline: "nearest" });
              }   
              

              这个代码很常见:

              componentDidMount() {
                this.scrollToBottom();
              }
              
              componentDidUpdate() {
                this.scrollToBottom();
              }
              
              
              render () {
                return (
                  <div>
                    <div className="MessageContainer" >
                      <div className="MessagesList">
                        {this.renderMessages()}
                      </div>
                      <div style={{ float:"left", clear: "both" }}
                           ref={(el) => { this.messagesEnd = el; }}>
                      </div>
                    </div>
                  </div>
                );
              }
              

              【讨论】:

                【解决方案15】:

                如果你想用 React Hooks 做到这一点,可以按照这个方法。对于虚拟 div 已放置在聊天的底部。此处使用了 useRef Hook。

                Hooks API 参考:https://reactjs.org/docs/hooks-reference.html#useref

                import React, { useEffect, useRef } from 'react';
                
                const ChatView = ({ ...props }) => {
                const el = useRef(null);
                
                useEffect(() => {
                    el.current.scrollIntoView({ block: 'end', behavior: 'smooth' });
                });
                
                 return (
                   <div>
                     <div className="MessageContainer" >
                       <div className="MessagesList">
                         {this.renderMessages()}
                       </div>
                       <div id={'el'} ref={el}>
                       </div>
                     </div>
                    </div>
                  );
                }
                

                【讨论】:

                  【解决方案16】:

                  我无法获得以下任何答案,但简单的 js 为我解决了问题:

                    window.scrollTo({
                    top: document.body.scrollHeight,
                    left: 0,
                    behavior: 'smooth'
                  });
                  

                  【讨论】:

                    【解决方案17】:

                    我推荐的最简单和最好的方法是。

                    我的 ReactJS 版本:16.12.0


                    对于类组件

                    HTML结构render()函数内

                        render()
                            return(
                                <body>
                                    <div ref="messageList">
                                        <div>Message 1</div>
                                        <div>Message 2</div>
                                        <div>Message 3</div>
                                    </div>
                                </body>
                            )
                        )
                    

                    scrollToBottom() 函数将获取元素的引用。 并根据scrollIntoView()函数滚动。

                      scrollToBottom = () => {
                        const { messageList } = this.refs;
                        messageList.scrollIntoView({behavior: "smooth", block: "end", inline: "nearest"});
                      }
                    

                    并在componentDidMount()componentDidUpdate()中调用上述函数


                    对于功能组件(钩子)

                    导入useRef()useEffect()

                    import { useEffect, useRef } from 'react'
                    

                    在您的导出函数中,(与调用 useState() 相同)

                    const messageRef = useRef();
                    

                    假设您必须在页面加载时滚动,

                    useEffect(() => {
                        if (messageRef.current) {
                          messageRef.current.scrollIntoView(
                            {
                              behavior: 'smooth',
                              block: 'end',
                              inline: 'nearest'
                            })
                        }
                      })
                    

                    或者,如果您希望它在执行操作后触发,

                    useEffect(() => {
                      if (messageRef.current) {
                        messageRef.current.scrollIntoView(
                          {
                            behavior: 'smooth',
                            block: 'end',
                            inline: 'nearest'
                          })
                      }
                    },
                    [stateVariable])
                    

                    最后,到你的 HTML 结构

                    return(
                        <body>
                            <div ref={messageRef}> // <= The only different is we are calling a variable here
                                <div>Message 1</div>
                                <div>Message 2</div>
                                <div>Message 3</div>
                            </div>
                        </body>
                    )
                    

                    更多关于Element.scrollIntoView()的解释请访问developer.mozilla.org

                    Callback refs中更详细的解释请访问reactjs.org

                    【讨论】:

                    • 该 ref 实际上应该在消息 div 中声明,而不是在容器中
                    【解决方案18】:

                    使用React.createRef()

                    class MessageBox extends Component {
                            constructor(props) {
                                super(props)
                                this.boxRef = React.createRef()
                            }
                    
                            scrollToBottom = () => {
                                this.boxRef.current.scrollTop = this.boxRef.current.scrollHeight
                            }
                    
                            componentDidUpdate = () => {
                                this.scrollToBottom()
                            }
                    
                            render() {
                                return (
                                            <div ref={this.boxRef}></div>
                                        )
                            }
                    }
                    

                    【讨论】:

                    • 这不会完全滚动到底部,请您指教
                    【解决方案19】:

                    这就是你在 TypeScript 中解决这个问题的方法(使用 ref 指向你滚动到的目标元素):

                    class Chat extends Component <TextChatPropsType, TextChatStateType> {
                      private scrollTarget = React.createRef<HTMLDivElement>();
                      componentDidMount() {
                        this.scrollToBottom();//scroll to bottom on mount
                      }
                    
                      componentDidUpdate() {
                        this.scrollToBottom();//scroll to bottom when new message was added
                      }
                    
                      scrollToBottom = () => {
                        const node: HTMLDivElement | null = this.scrollTarget.current; //get the element via ref
                    
                        if (node) { //current ref can be null, so we have to check
                            node.scrollIntoView({behavior: 'smooth'}); //scroll to the targeted element
                        }
                      };
                    
                      render <div>
                        {message.map((m: Message) => <ChatMessage key={`chat--${m.id}`} message={m}/>}
                         <div ref={this.scrollTarget} data-explanation="This is where we scroll to"></div>
                       </div>
                    }
                    

                    有关在 React 和 Typescript 中使用 ref 的更多信息,您可以找到一篇很棒的文章 here

                    【讨论】:

                      【解决方案20】:

                      这是根据上面的答案修改的,以支持“孩子”而不是数据数组。

                      注意:样式组件的使用对解决方案并不重要。

                      import {useEffect, useRef} from "react";
                      import React from "react";
                      import styled from "styled-components";
                      
                      export interface Props {
                          children: Array<any> | any,
                      }
                      
                      export function AutoScrollList(props: Props) {
                          const bottomRef: any = useRef();
                      
                          const scrollToBottom = () => {
                              bottomRef.current.scrollIntoView({
                                  behavior: "smooth",
                                  block: "start",
                              });
                          };
                      
                          useEffect(() => {
                              scrollToBottom()
                          }, [props.children])
                      
                          return (
                              <Container {...props}>
                                  <div key={'child'}>{props.children}</div>
                                  <div key={'dummy'} ref={bottomRef}/>
                              </Container>
                          );
                      }
                      
                      const Container = styled.div``;
                      

                      【讨论】:

                        【解决方案21】:

                        为了首先向下滚动到页面底部,我们必须选择一个位于页面底部的 id。然后我们可以使用 document.getElementById 选择 id 并使用 scrollIntoView() 向下滚动。请参考以下代码。

                           scrollToBottom= async ()=>{
                              document.getElementById('bottomID').scrollIntoView();
                            }
                        

                        【讨论】:

                          【解决方案22】:

                          这对我有用

                          messagesEndRef.current.scrollTop = messagesEndRef.current.scrollHeight

                          其中 const messagesEndRef = useRef();使用

                          【讨论】:

                            【解决方案23】:

                            顶级答案中的 scrollIntoView(...) 方法存在两个主要问题:

                            1. 这在语义上是不正确的,因为如果您的父元素滚动到窗口边界之外,它会导致整个页面滚动。浏览器会滚动任何它需要的内容以使元素可见。

                            2. 在使用 useEffect() 的功能组件中,您会得到不可靠的结果,至少在 Chrome 96.0.4665.45 中是这样。在页面重新加载时过早调用 useEffect() 并且不会发生滚动。使用 setTimeout(..., 0) 延迟 scrollIntoView 可以修复页面重新加载,但不是首先加载到新选项卡中,至少对我而言。 耸耸肩

                            这是我一直在使用的解决方案,它很可靠,并且与旧版浏览器更兼容:

                            function Chat() {
                               const chatParent = useRef<HTMLDivElement(null);
                            
                               useEffect(() => {
                                  const domNode = chatParent.current;
                                  if (domNode) {
                                     domNode.scrollTop = domNode.scrollHeight;
                                  }
                               })
                               return (
                                  <div ref={chatParent}>
                                     ...
                                  </div>
                               )
                            }
                            

                            【讨论】:

                            • 这很好用!另外,我会在进行自动滚动时添加一些阈值,如果用户正在查看历史记录,您不想滚动,对吧?
                            • @Crysfel 我同意,但我不知道浏览器是否有滚动开始/结束事件,在这种情况下,如果不实现自己的滚动条或破解某些东西,就没有干净的方法来解决这个问题。可能的技巧:使用滚动事件和计时器,使用 mousedown/mouseup 和一些猜测,或者在鼠标按下或聊天窗口有焦点时根本不滚动。
                            猜你喜欢
                            • 1970-01-01
                            • 2014-12-12
                            • 1970-01-01
                            • 2021-02-24
                            • 2012-02-06
                            • 2020-12-09
                            • 1970-01-01
                            • 2011-09-16
                            • 1970-01-01
                            相关资源
                            最近更新 更多