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


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

如果你知道如何设置单个组件的引用(最后一个),请发布!

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

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();
    }
  }
}

我在消息的末尾创建了一个空元素,并滚动到该元素。不需要跟踪裁判。


正如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();
}

我使用的是标准元素。scrollIntoView方法。


感谢@enlitement

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

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/


Reference your messages container. <div ref={(el) => { this.messagesContainer = el; }}> YOUR MESSAGES </div> Find your messages container and make its scrollTop attribute equal scrollHeight: scrollToBottom = () => { const messagesContainer = ReactDOM.findDOMNode(this.messagesContainer); messagesContainer.scrollTop = messagesContainer.scrollHeight; }; Evoke above method on componentDidMount and componentDidUpdate. 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>
        );
    }
}

另一个值得考虑的选择是react scroll组件。


我喜欢这样做。

componentDidUpdate(prevProps, prevState){
  this.scrollToBottom();
}

scrollToBottom() {
  const {thing} = this.refs;
  thing.scrollTop = thing.scrollHeight - thing.clientHeight;
}

render(){
  return(
    <div ref={`thing`}>
      <ManyThings things={}>
    </div>
  )
}

不使用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 divRef = useRef(null);

  useEffect(() => {
    divRef.current.scrollIntoView({ behavior: 'smooth' });
  });

  return <div ref={divRef} />;
}

工作的例子:

您可以使用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是作为道具传递的特定聊天消息的唯一ID


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>
        );
    }
}

我只是想更新答案以匹配新的React.createRef()方法,但它基本上是相同的,只要记住在创建的ref中的当前属性:

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>
    )
  }
}

更新:

现在钩子可用了,我正在更新答案,添加useRef和useEffect钩子的使用,真正做魔法的事情(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


如果用户已经在可滚动部分的底部,react- scrolable -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>
    );
  }
}

只要确保包装器组件具有特定的高度或max-height即可

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


完整版(Typescript):

import * as React from React 导出类DivWithScrollHere扩展React。组件<any, any> { loading:any = React.createRef(); componentDidMount () { this.loading.scrollIntoView(假); } 呈现(){ 回报( <div ref={e =>{这个。装载= e;}}> <LoadingTile /> </div> . txt ) } }


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

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>
  );
}

如果你想用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>
  );
}

我不能得到以下任何答案的工作,但简单的js为我做的伎俩:

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

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

我的ReactJS版本:16.12.0


对于类组件

render()函数内的HTML结构

    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

更详细的解释在回调参考访问reactjs.org


使用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>
                    )
        }
}

这是你在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的更多信息,你可以在这里找到一篇很棒的文章。


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

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

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``;

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

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

这对我很有用

messagesEndRef.current.scrollTop = messagesEndRef.current.scrollHeight

const messagesEndRef = useRef();使用


在上面的答案中,scrollIntoView(…)方法有两个主要问题:

it's semantically incorrect, as it causes the entire page to scroll if your parent element is scrolled outside the window boundaries. The browser literally scrolls anything it needs to in getting the element visible. in a functional component using useEffect(), you get unreliable results, at least in Chrome 96.0.4665.45. useEffect() gets called too soon on page reload and the scroll doesn't happen. Delaying scrollIntoView with setTimeout(..., 0) fixes it for page reload, but not first load in a fresh tab, at least for me. shrugs

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

function Chat() {
   const chatParent = useRef<HTMLDivElement(null);

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

我在mweb/web中遇到过这个问题。所有的解决方案是很好的在这个页面,但所有的解决方案是不工作,而使用android chrome浏览器。 所以对于mweb和web,我得到了一些小的解决方案。

    import { createRef, useEffect } from 'react';
    import { useSelector } from 'react-redux';
    import { AppState } from 'redux/store';
    import Message from '../Message/Message';
    import styles from './MessageList.module.scss';
    
    const MessageList = () => {
      const messagesEndRef: any = createRef();
      const { messages } = useSelector((state: AppState) => state?.video);
      const scrollToBottom = () => {
          //this is not working in mWeb
            // messagesEndRef.current.scrollIntoView({
            //   behavior: 'smooth',
            //   block: 'end',
            //   inline: 'nearest',
            // });
         const scroll =
          messagesEndRef.current.scrollHeight -
           messagesEndRef.current.clientHeight;
         messagesEndRef.current.scrollTo(0, scroll);
      };
    
      useEffect(() => {
        if (messages.length > 3) {
          scrollToBottom();
        }
      }, [messages]);
    
      return (
        <section className={styles.footerTopSection} ref={messagesEndRef} >
          {messages?.map((message: any) => (
            <Message  key={message.id} {...message} />
          ))}
        </section>
      );
    };
    
    export default MessageList;

这是Kent C. Dodds教授的useLayoutEffect的一个很好的用例。

https://kentcdodds.com/blog/useeffect-vs-uselayouteffect

如果你的效果正在改变DOM(通过一个DOM节点ref),并且DOM突变将改变DOM节点的外观,在它被呈现和你的效果改变它之间,那么你不想使用useEffect。

在我的情况下,我在一个div的底部动态生成元素,所以我必须添加一个小超时。

   const bottomRef = useRef<null | HTMLDivElement>(null);

    useLayoutEffect(() => {
        setTimeout(function () {
            if (bottomRef.current) bottomRef.current.scrollTop = bottomRef.current.scrollHeight;
        }, 10);
    }, [transactionsAmount]);

 const scrollingBottom = () => {
    const e = ref;

    e.current?.scrollIntoView({
      behavior: "smooth",
      block: "center",
      inline: "start",
    });
  };

 useEffect(() => {
    scrollingBottom();
  });

<span ref={ref}>{item.body.content}</span>