【发布时间】:2016-10-03 21:25:47
【问题描述】:
我想搭建一个聊天系统,在进入窗口和有新消息进来时自动滚动到底部。React中如何自动滚动到容器底部?
【问题讨论】:
标签: reactjs
我想搭建一个聊天系统,在进入窗口和有新消息进来时自动滚动到底部。React中如何自动滚动到容器底部?
【问题讨论】:
标签: reactjs
您可以使用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();
}
}
}
【讨论】:
我在消息末尾创建了一个空元素,然后滚动到该元素。无需跟踪 refs。
【讨论】:
正如 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 方法。
【讨论】:
this.messagesEnd.scrollIntoView() 对我来说效果很好。没有必要使用findDOMNode()。
感谢@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();
}
参考:
【讨论】:
引用您的消息容器。
<div ref={(el) => { this.messagesContainer = el; }}> YOUR MESSAGES </div>
找到您的消息容器并使其scrollTop 属性等于scrollHeight:
scrollToBottom = () => {
const messagesContainer = ReactDOM.findDOMNode(this.messagesContainer);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
};
在componentDidMount 和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
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} />;
}
【讨论】:
behavior 的美式拼写(不能编辑,因为“编辑必须至少有 6 个字符”,叹息)。
scrollIntoView 和smooth 的支持很差。
工作示例:
您可以使用 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
【讨论】:
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()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>
)
}
}
更新:
现在钩子可用,我正在更新答案以添加 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,还制作了一个(非常基本的)代码框
【讨论】:
useEffect方法需要和() => {scrollToBottom()}放在一起。无论如何都非常感谢
null,直到渲染。为了解决这个问题,我将if (messagesEndRef.current) 放在`scrollToBottom 函数中。
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>
);
}
}
只需确保有一个带有特定height 或max-height 的包装器组件
免责声明:我是包的所有者
【讨论】:
ScrollIntoView 只是滚动一点,而不是滚动到底部。
完整版(打字稿):
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<unknown>'. 和 Type 'HTMLDivElement | null' is not assignable to type 'RefObject<unknown>'. Type 'null' is not assignable to type 'RefObject<unknown>'. 所以...
感谢 '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>
);
}
【讨论】:
如果你想用 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
对于类组件
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
【讨论】:
使用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 的更多信息,您可以找到一篇很棒的文章 here。
【讨论】:
这是根据上面的答案修改的,以支持“孩子”而不是数据数组。
注意:样式组件的使用对解决方案并不重要。
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。然后我们可以使用 document.getElementById 选择 id 并使用 scrollIntoView() 向下滚动。请参考以下代码。
scrollToBottom= async ()=>{
document.getElementById('bottomID').scrollIntoView();
}
【讨论】:
这对我有用
messagesEndRef.current.scrollTop = messagesEndRef.current.scrollHeight
其中 const messagesEndRef = useRef();使用
【讨论】:
顶级答案中的 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>
)
}
【讨论】: