如何在反应中滚动到底部?
Posted
技术标签:
【中文标题】如何在反应中滚动到底部?【英文标题】:How to scroll to bottom in react? 【发布时间】:2016-10-03 21:25:47 【问题描述】:我想搭建一个聊天系统,在进入窗口和有新消息进来时自动滚动到底部。React中如何自动滚动到容器底部?
【问题讨论】:
【参考方案1】:您可以使用ref
s 来跟踪组件。
如果您知道设置单个组件(最后一个)的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;
;
在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>
);
【讨论】:
【参考方案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 个字符”,叹息)。
目前对scrollIntoView
和smooth
的支持很差。
对我很有帮助的帖子!【参考方案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>
)
更新:
现在钩子可用,我正在更新答案以添加 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,还制作了一个(非常基本的)代码框
【讨论】:
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
方法需要和() => scrollToBottom()
放在一起。无论如何都非常感谢
我对@987654332@ 的这个实现有一个错误:“当前”是null
,直到渲染。为了解决这个问题,我将if (messagesEndRef.current)
放在`scrollToBottom 函数中。
修复打字稿错误“打字稿错误:“从不”类型上不存在属性“scrollIntoView”。 TS2339' -> 使用 useRef 分配正确的类型: const scrollRef = useRefreact-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
的包装器组件
免责声明:我是包的所有者
【讨论】:
谢谢,我使用了你的控件。注意:我必须使用 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<unknown>'.
和 Type 'HTMLDivElement | null' is not assignable to type 'RefObject<unknown>'. Type 'null' is not assignable to type 'RefObject<unknown>'.
所以...
请问 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 在动态高度单元格中滚动到底部?