如何在反应中滚动到底部?

Posted

技术标签:

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

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

【问题讨论】:

【参考方案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();

参考:

https://facebook.github.io/react/docs/react-dom.html#finddomnode https://www.pubnub.com/blog/2016-06-28-reactjs-chat-app-infinite-scroll-history-using-redux/

【讨论】:

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

    引用您的消息容器。

    <div ref=(el) =>  this.messagesContainer = el; > YOUR MESSAGES </div>
    

    找到您的消息容器并使其scrollTop 属性等于scrollHeight

    scrollToBottom = () => 
        const messagesContainer = ReactDOM.findDOMNode(this.messagesContainer);
        messagesContainer.scrollTop = messagesContainer.scrollHeight;
    ;
    

    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(...) 方法存在两个主要问题:

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

    在使用 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 和一些猜测,或者在鼠标按下或聊天窗口有焦点时根本不滚动。

以上是关于如何在反应中滚动到底部?的主要内容,如果未能解决你的问题,请参考以下文章

如何让 UITableView 在动态高度单元格中滚动到底部?

我们如何在反应原生版本 .61 中将动画添加到底部选项卡图标

如何自动滚动到底部,如果在底部则滚动到顶部

如何使用反应路由器 dom v6 在路由更改时滚动到顶部?

如何触发jScrollPane滚动到底部事件

如何以编程方式将 ScrollView 滚动到底部