Open duhaime opened 6 years ago
I can confirm this behavior in my local dev environment. It appears that the scroll event is caught in some loop. This also makes anything un-clickable while the glitch occurs.
The source of the bug: every scroll event callback is calling .setState which reRender the component. and it turns two problems:
I also found this problem in the project. Do you have any good ways?
I can confirm this behavior is happening to a project of mine as well. @ido1wald seems to be onto something. I'm not handling any events. Anyone has a solution to this?
@duhaime @bj7 @ido1wald @BlackSkyJY @KendallWeihe Is anyone found any solution? I have it too and it's blocker, and I even can't find any hack-around...
@Timopheym sorry brother, we re-created this entire component in our private repo, from scratch. it isn't easy task, just don't use this library if it's important to you
@ido1wald Godness... is there any open-source alternative or may be not buggy tag of this one?...
@Timopheym I too would roll my own. If you did and it were general enough you could open source it and carry the mantle!
@duhaime I managed it with integration of https://github.com/bvaughn/react-virtualized/ currently i have some issues, but i hope to make well.
The idea behind this component is surely interesting. At any given moment only some of the items are mounted. The rest are replaced by two divs (top and bottom spacers) of height the corresponding items would take. Initially only, say, the first 5 is mounted (topSpacerHeight == 0
). As you scroll down the 1st gets unmounted, the 6th gets mounted. Then, the 2nd, and the 7th. And so on.
This component might suit well cases where heights of the items are known in advance. But when they aren't, especially when the items contain images (not to mention videos), it takes much more effort to make it work.
What's the big deal about providing accurate heights of the items? The thing is that otherwise every time you cross the line where the first item (of the mounted bunch) gets unmounted, and the next one (after the mounted bunch) gets mounted, the thumb of the scrollbar twitches. Since topSpacerHeight + displayedItemsHeight + bottomSpacerHeight
changes.
Also if you mess up to provide react-infinite
with exact heights, you're bound to experience the effect described in this issue. You cross the line. Scroll handler notices this, and arranges for mounted items window to move down one item (first item unmounted, next mounted). As a result, height of the list of the items changes (inaccurate heights). For some reason scroll event gets triggered here. And the handler notices the line has been crossed back. Therefore it makes the window (list of mounted items) move up. This goes on until you drag the thumb (of the scrollbar) away from this position.
Let's see what issues I've faced. Imagine a list of posts, handled by two components: Posts
, and Post
. Posts contain images, are separated by horizontal lines and some space:
Posts.js
:
import React from 'react';
import Infinite from 'react-infinite';
import Post from './Post';
const defaultHeight = 300;
export default class Posts extends React.Component {
constructor(props) {
super(props);
this.state = {
posts: [],
isInfiniteLoading: false,
elementHeights: defaultHeight,
};
this.noMorePosts = false;
this.offset = 0;
this.pageSize = 5;
this.postHeights = {};
}
componentDidMount() {
this.fetchPosts();
}
fetchPosts() {
api(this.offset, this.pageSize).then(({data}) => {
const newElementHeights = Array(data.length).fill(defaultHeight);
this.setState(prevState => ({
posts: prevState.posts.concat(data),
elementHeights: typeof prevState.elementHeights === 'number'
? newElementHeights
: prevState.elementHeights.concat(newElementHeights),
isInfiniteLoading: false,
}));
this.noMorePosts = data.length === 0;
})
this.offset += this.pageSize;
}
// posts report their heights to the Posts component
handlePostHeight = (id, i, height) => {
if (id in this.postHeights)
return;
this.postHeights[id] = height;
if (typeof this.state.elementHeights === 'number')
this.setState({elementHeights:
Array(this.state.posts.length).fill(defaultHeight)
.fill(height, i, i + 1)});
else
this.setState(prevState => ({elementHeights:
prevState.elementHeights.fill(height, i, i + 1)}));
}
handleInfiniteLoad = () => {
if (this.state.isInfiniteLoading || this.noMorePosts)
return;
this.setState({isInfiniteLoading: true});
this.fetchPosts();
}
// not a good way to use callback refs probably
handleCallbackRef = post => {
if ('postSpacingHeight' in this)
return;
this.postSpacingHeight = 0; // this line makes it reproducable (inaccurate height)
if (post.props.i !== 0) {
// here we get reference to non-first post
// the one which has, say, margin-top
// or in other words, non-zero spacing height
this.postSpacingHeight = post.getSpacingHeight();
}
}
render() {
return <div>
<Infinite elementHeight={this.state.elementHeights}
onInfiniteLoad={this.handleInfiniteLoad}
infiniteLoadBeginEdgeOffset={200}
useWindowAsScrollContainer={true}
isInfiniteLoading={this.state.isInfiniteLoading}>
{this.state.posts.map((p, i) =>
<Post i={i} key={p.id} attrs={p}
minHeight={
/* set min-height after height is known
to set it right away next time it mounts
to not wait for height to be calculated
to not wait for image in a post to load */
p.id in this.postHeights
? this.postHeights[p.id] - this.postSpacingHeight || 0 + 'px'
: 'auto'
}
onPostHeight={this.handlePostHeight}
ref={this.handleCallbackRef}/>
)}
</Infinite>
</div>;
}
}
const heights = [4, 89, 166, 315, 436, 23, 153, 337, 157, 340, 563, 417, 315, 351, 28, 156, 64, 123, 555, 578, 501, 218, 429, 559, 229, 213, 243, 346, 119, 521, 142, 102, 534, 313, 398, 385, 407, 230, 307, 472, 26, 374, 411, 495, 303, 397, 369, 232, 75, 340];
function api(offset, pageSize) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
data: heights.slice(offset, offset + pageSize).map((h, i) => ({
id: offset + i + 1,
url: `http://via.placeholder.com/300x${h}`,
}))
});
}, Math.random() * 2000);
});
}
Post.js
:
import React from 'react';
import imagesLoaded from 'imagesloaded';
import computedStyle from 'computed-style';
import classNames from 'classnames';
import './Post.css';
export default class Post extends React.Component {
constructor(props) {
super(props);
this.selfRef = React.createRef();
this.wrapperRef = React.createRef();
}
componentDidMount() {
this.mounted = true;
imagesLoaded(this.selfRef.current, () => {
if (this.mounted) { // image might finish loading after component has unmounted
this.props.onPostHeight(this.props.attrs.id, this.props.i,
this.selfRef.current.offsetHeight);
}
});
}
componentWillUnmount() {
this.mounted = false;
}
getSpacingHeight() {
return this.selfRef.current.offsetHeight
- (this.wrapperRef.current.offsetHeight
- parseFloat(computedStyle(this.wrapperRef.current, 'border-top-width'))
- parseFloat(computedStyle(this.wrapperRef.current, 'padding-top')));
}
render() {
return <div
className={classNames('post', this.props.i === 0 && 'first')}
data-i={this.props.i} ref={this.selfRef}>
<div className="wrapper" ref={this.wrapperRef} style={{
minHeight: this.props.minHeight,
}}>
<img src={this.props.attrs.url} alt=""/>
</div>
</div>;
}
}
Post.css
:
.post {
padding-top: 20px;
}
.post.first {
padding-top: 0;
}
.wrapper {
border-top: 1px solid #999;
padding-top: 20px;
}
.post.first .wrapper {
border-top: none;
padding-top: 0;
}
.post img {
display: block;
margin: auto;
}
Click down arrow until it starts to twitch. You can find the needed files in this archive.
Do note, every time heights change (add, edit, delete), you've got to make them known to react-infinite
.
I seem to have managed to make it work, but the code is way too complex. I decided, that if the user scrolls that much that it affects performance, the issue is either with the user (doesn't know when to give up), or with the user interface (lack of appropriate filters). So I'm going to try one of these.
Just FYI, the demo page displays a strange rapid glitch when a user has scrolled to a particular location on the page:
As you can see at the end of the gif, the glitching disappears if one scrolls up (or down) just slightly from the mysterious spot.
This was observed in recent Chrome on OSX.