[英]How to implement a React infinite scroll component for a chat app like Facebook Messenger?
I know there are many questions about this topic (React infinite scroll), my question aims to go more in-depth in order to identify the best currently available solution to implement such a component.我知道关于这个主题有很多问题(React 无限滚动),我的问题旨在更深入地确定当前可用的最佳解决方案来实现这样的组件。
I am working on a chat app and I have created a component similar to the Facebook's Messenger chat window which you can see on desktop browsers.我正在开发一个聊天应用程序,我创建了一个类似于 Facebook Messenger 聊天窗口的组件,您可以在桌面浏览器上看到它。
Facebook: Facebook:
Mine (so far):我的(到目前为止):
Implementing the infinite scroll with infinite loading turns out to be tricky.实现无限加载的无限滚动结果是很棘手的。 From a UX perspective, I need to always satisfy at least the following properties:
从用户体验的角度来看,我需要始终至少满足以下属性:
Now, in order to do that, I have already tried several libraries:现在,为了做到这一点,我已经尝试了几个库:
react-list
component. react-list
组件的错误。 Also, the component does not allow me to display the scrolling bottom upwards (see https://github.com/coderiety/react-list/issues/50 );List
with an InfiniteLoader
together with AutoSizer
, CellMeasurer
and CellMeasurerCache
. List
与InfiniteLoader
与AutoSizer
、 CellMeasurer
和CellMeasurerCache
一起使用很棘手。 Also, as I send a message if I call List.scrollToIndex(lastIndex)
to scroll automatically the container to the bottom the scroll does not reach the bottom completely, as the scrollable container has top and bottom padding.List.scrollToIndex(lastIndex)
自动将容器滚动到底部,滚动不会完全到达底部,因为可滚动容器具有顶部和底部填充。 I couldn't achieve a satisfiable result with this component. So my question is more a way to confront each other: have someone of you ever had to implement a React chat component with the 3 requirements I have written above?所以我的问题更像是一种相互对抗的方式:你们中的某个人是否曾经需要根据我上面写的 3 个要求来实现一个 React 聊天组件? What library did you use?
你用的什么库? As Facebook Messenger handles this pretty well and they use React, do someone of you know how did they implement such a component?
由于 Facebook Messenger 处理得很好并且他们使用 React,你们中有人知道他们是如何实现这样的组件的吗? If I inspect the chat messages of the Facebook chat window it seems that it keeps all the already rendered messages in the DOM.
如果我检查 Facebook 聊天窗口的聊天消息,它似乎将所有已呈现的消息保留在 DOM 中。 But, if so, couldn't this affect performance?
但是,如果是这样,这不会影响性能吗?
So I have more questions than answers for now.所以我现在的问题多于答案。 I would really like to find a component that suits my needs.
我真的很想找到一个适合我需要的组件。 The other option would be to implement my own.
另一种选择是实现我自己的。
I ended up implementing my own very simple infinite scroll component (didn't refactor it to use hooks yet, though):我最终实现了我自己的非常简单的无限滚动组件(不过还没有重构它以使用钩子):
import React from "react";
import {
isUndefined,
hasVerticalScrollbar,
hasHorizontalScrollbar,
isInt,
debounce
} from "js-utl";
import { classNames } from "react-js-utl/utils";
export default class SimpleInfiniteScroll extends React.Component {
constructor(props) {
super(props);
this.handleScroll = this.handleScroll.bind(this);
this.onScrollStop = debounce(this.onScrollStop.bind(this), 100);
this.itemsIdsRefsMap = {};
this.isLoading = false;
this.isScrolling = false;
this.lastScrollStopPromise = null;
this.lastScrollStopPromiseResolve = null;
this.node = React.createRef();
}
componentDidMount() {
this.scrollToStart();
}
getNode() {
return this.node && this.node.current;
}
getSnapshotBeforeUpdate(prevProps) {
if (prevProps.children.length < this.props.children.length) {
const list = this.node.current;
const axis = this.axis();
const scrollDimProperty = this.scrollDimProperty(axis);
const scrollProperty = this.scrollProperty(axis);
const scrollDelta = list[scrollDimProperty] - list[scrollProperty];
return {
scrollDelta
};
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (
this.isLoading &&
((prevProps.isInfiniteLoading && !this.props.isInfiniteLoading) ||
((this.props.hasMore || prevProps.hasMore) &&
prevProps.children.length !==
this.props.children.length)) &&
snapshot
) {
if (this.props.displayInverse) {
const list = this.node.current;
const axis = this.axis();
const scrollDimProperty = this.scrollDimProperty(axis);
const scrollProperty = this.scrollProperty(axis);
const scrollDelta = snapshot.scrollDelta;
const scrollTo = list[scrollDimProperty] - scrollDelta;
this.scrollTo(scrollProperty, scrollTo);
}
this.isLoading = false;
}
}
loadingComponentRenderer() {
const { loadingComponent } = this.props;
return (
<div
className="simple-infinite-scroll-loading-component"
key={-2}
>
{loadingComponent}
</div>
);
}
axis() {
return this.props.axis === "x" ? "x" : "y";
}
scrollProperty(axis) {
return axis === "y" ? "scrollTop" : "scrollLeft";
}
offsetProperty(axis) {
return axis === "y" ? "offsetHeight" : "offsetWidth";
}
clientDimProperty(axis) {
return axis === "y" ? "clientHeight" : "clientWidth";
}
scrollDimProperty(axis) {
return axis === "y" ? "scrollHeight" : "scrollWidth";
}
hasScrollbarFunction(axis) {
return axis === "y" ? hasVerticalScrollbar : hasHorizontalScrollbar;
}
scrollToStart() {
const axis = this.axis();
this.scrollTo(
this.scrollProperty(axis),
!this.props.displayInverse ? 0 : this.scrollDimProperty(axis)
);
}
scrollToEnd() {
const axis = this.axis();
this.scrollTo(
this.scrollProperty(axis),
!this.props.displayInverse ? this.scrollDimProperty(axis) : 0
);
}
scrollTo(scrollProperty, scrollPositionOrPropertyOfScrollable) {
const scrollableContentNode = this.node.current;
if (scrollableContentNode) {
scrollableContentNode[scrollProperty] = isInt(
scrollPositionOrPropertyOfScrollable
)
? scrollPositionOrPropertyOfScrollable
: scrollableContentNode[scrollPositionOrPropertyOfScrollable];
}
}
scrollToId(id) {
if (this.itemsIdsRefsMap[id] && this.itemsIdsRefsMap[id].current) {
this.itemsIdsRefsMap[id].current.scrollIntoView();
}
}
scrollStopPromise() {
return (
(this.isScrolling && this.lastScrollStopPromise) ||
Promise.resolve()
);
}
onScrollStop(callback) {
callback();
this.isScrolling = false;
this.lastScrollStopPromise = null;
this.lastScrollStopPromiseResolve = null;
}
handleScroll(e) {
const {
isInfiniteLoading,
hasMore,
infiniteLoadBeginEdgeOffset,
displayInverse
} = this.props;
this.isScrolling = true;
this.lastScrollStopPromise =
this.lastScrollStopPromise ||
new Promise(resolve => {
this.lastScrollStopPromiseResolve = resolve;
});
this.onScrollStop(() => {
this.lastScrollStopPromiseResolve &&
this.lastScrollStopPromiseResolve();
});
this.props.onScroll && this.props.onScroll(e);
if (
this.props.onInfiniteLoad &&
(!isUndefined(hasMore) ? hasMore : !isInfiniteLoading) &&
this.node.current &&
!this.isLoading
) {
const axis = this.axis();
const scrollableContentNode = this.node.current;
const scrollProperty = this.scrollProperty(axis);
const offsetProperty = this.offsetProperty(axis);
const scrollDimProperty = this.scrollDimProperty(axis);
const currentScroll = scrollableContentNode[scrollProperty];
const currentDim = scrollableContentNode[offsetProperty];
const scrollDim = scrollableContentNode[scrollDimProperty];
const finalInfiniteLoadBeginEdgeOffset = !isUndefined(
infiniteLoadBeginEdgeOffset
)
? infiniteLoadBeginEdgeOffset
: currentDim / 2;
let thresoldWasReached = false;
if (!displayInverse) {
const clientDimProperty = this.clientDimProperty(axis);
const clientDim = scrollableContentNode[clientDimProperty];
thresoldWasReached =
currentScroll +
clientDim +
finalInfiniteLoadBeginEdgeOffset >=
scrollDim;
} else {
thresoldWasReached =
currentScroll <= finalInfiniteLoadBeginEdgeOffset;
}
if (thresoldWasReached) {
this.isLoading = true;
this.props.onInfiniteLoad();
}
}
}
render() {
const {
children,
displayInverse,
isInfiniteLoading,
className,
hasMore
} = this.props;
return (
<div
className={classNames("simple-infinite-scroll", className)}
ref={this.node}
onScroll={this.handleScroll}
onMouseOver={this.props.onInfiniteScrollMouseOver}
onMouseOut={this.props.onInfiniteScrollMouseOut}
onMouseEnter={this.props.onInfiniteScrollMouseEnter}
onMouseLeave={this.props.onInfiniteScrollMouseLeave}
>
{(hasMore || isInfiniteLoading) &&
displayInverse &&
this.loadingComponentRenderer()}
{children}
{(hasMore || isInfiniteLoading) &&
!displayInverse &&
this.loadingComponentRenderer()}
</div>
);
}
}
And in this.props.children
I pass it an array of React elements of the following component's class which extends React.PureComponent
:在
this.props.children
我将以下组件类的 React 元素数组传递给它,该类扩展了React.PureComponent
:
...
export default class ChatMessage extends React.PureComponent {
...
}
This way, when re-rendering, only the components that have changed since the last render are re-rendered.这样,在重新渲染时,只会重新渲染自上次渲染以来发生更改的组件。
I have also used an immutable data structure to store the collection of the chat messages, in particularly immutable-linked-ordered-map
( https://github.com/tonix-tuft/immutable-linked-ordered-map ) which allows me to achieve O(1)
time complexity for insertions, removals and updates of a message as well as almost O(1)
time complexity for lookups.我还使用了一个不可变的数据结构来存储聊天消息的集合,特别是
immutable-linked-ordered-map
( https://github.com/tonix-tuft/immutable-linked-ordered-map ) 这让我实现O(1)
用于插入,删除和消息以及几乎更新的时间复杂度O(1)
时间复杂度的查找。 Essentially, ImmutableLinkedOrderedMap
is an ordered immutable map, like associative arrays in PHP, but immutable:本质上,
ImmutableLinkedOrderedMap
是一个有序的不可变映射,就像 PHP 中的关联数组一样,但不可变:
const map = new ImmutableLinkedOrderedMap({
mode: ImmutableLinkedOrderedMap.MODE.MULTIWAY,
initialItems: [
{
id: 1, // <--- "[keyPropName] === 'id'"
text: "Message text",
// ...
},
{
id: 2,
text: "Another message text",
// ...
},
// ...
]
})
map.get(2) // Will return: { id: 2, text: "Another message text", /* ... */ }
const newMessage = { id: 3, text: "Yet another message text", /* ... */ };
const newMap = map.set(newMessage);
console.log(map !== newMap); // true
console.log(map.length); // 2
console.log(newMap.length); // 3
let messages = newMap.replace(3, newMessage)
console.log(messages === newMap); // true, because newMessage with ID 3 didn't change
messages = newMap.replace(3, { ...newMessage, read: true })
console.log(messages === newMap); // false
Then, when I render the messages stored in the map, I simply call its .values()
method which returns an array and I map that array to render the messages, eg:然后,当我渲染存储在地图中的消息时,我只需调用它的
.values()
方法,该方法返回一个数组并映射该数组以渲染消息,例如:
<SimpleInfiniteScroll>
{messages.values().map((message) => <ChatMessage ... />)}
</SimpleInfiniteScroll>
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.