react-infinite icon indicating copy to clipboard operation
react-infinite copied to clipboard

Container Height continues to increase while at bottom of page. Causes jerky scrolling when scrolling up.

Open jgoodall628 opened this issue 8 years ago • 18 comments

I am using the window as the scoll container. When I have scrolled to the bottom. I can see the containing div continuing to increase even though there are no new elements to be added. This causes scrolling up to be jerky as react infinite readjust the user to where it thinks it should be in the scroll. Any ideas on how to solve this?

jgoodall628 avatar May 08 '17 22:05 jgoodall628

This is true on my side also, and I am not using the window as the scroll container. Super annoying/funky.

SmboBeast avatar May 15 '17 06:05 SmboBeast

Just solved this problem in my own application. The problem was that the immediate child of my <Infinite> component was a single table element that contained my repeatable elements. I solved by setting props.elementHeight and props.preloadBatchSize to the total height of the table element (calculated in componentWillReceiveProps).

ericdvb avatar May 19 '17 14:05 ericdvb

@ericdvb I am having the same problem. Can you please explain your solution a little bit more, as it is not clear how it is fixing this issue.

Jaikant avatar Jun 06 '17 03:06 Jaikant

@Jaikant Initially I started with:

<Infinite
  elementHeight={rowHeight}
  containerHeight={tableBodyHeight}
  preloadBatchSize={rowHeight*numberOfInitialRows}
>
  <Table>
    <tbody>
      { tableRows }
    </TableBody>
  </Table>
</Infinite>

but what worked is:

<Infinite
  elementHeight={rowHeight*props.results.length}
  containerHeight={tableBodyHeight}
  preloadBatchSize={rowHeight*props.results.length}
>
  <Table>
    <tbody>
      { tableRows }
    </tbody>
  </Table>
</Infinite>

The direct child of my <Infinite> component is a single <Table> element, which contains the repeatable elements (<tr>s in my case). I initially tried to pass the height of a single <tr> element to <Infinite> as the prop elementHeight, and the height of a single element * the number of initial elements as prop preloadBatchSize. In my experience, this is not what <Infinite> expects, and causes <Infinite> to continue expanding its height indefinitely after you reach the scroll threshold the first time.

I didn't look into what's happening internally, but what worked for me was passing the height of whatever the direct children of <Infinite> were as both the props preloadBatchSize and elementHeight (in my case, that meant calculating the total height of the <Table> element by multiplying the number of repeatable results * the height of a single row). Make sense? If your <Infinite> element has an array of children instead of a single element, this may not apply to you.

ericdvb avatar Jun 06 '17 16:06 ericdvb

@ericdvb Thanks for the explanation. I was curious about the answer also.

So, it looks like your rows are all the same height, so it's easy to figure this out.

In my case, the heights of the rows vary, which makes things even more complicated. Any tips on how to calculate each row's height somehow? I assume the only real way would be to render my row, calculate the height, rerender. Sounds sloppy.

Thanks, Sam

SmboBeast avatar Jun 06 '17 16:06 SmboBeast

@SmboBeast No problem. If the row Height is totally dynamic, yes, I think that's the most idiomatic way using this library. Another option I see floating around is to create a canvas element, render your elements to that, and then measure their heights there.

There are a couple of other libraries out there that are designed to support infinite scrolling with variable element heights, but neither of the demos works the way I would expect:

https://github.com/Radivarig/react-infinite-any-height https://github.com/tnrich/react-variable-height-infinite-scroller

ericdvb avatar Jun 06 '17 16:06 ericdvb

@ericdvb This makes total sense.

As for the other libraries, I too have hit trouble with just the demos themselves, so I disregarded them essentially.

Thanks for your help. My solution might be a hybrid of what you did and calculating the row heights dynamically. Let me give it a go.

Sam

SmboBeast avatar Jun 06 '17 16:06 SmboBeast

@SmboBeast come back and let us know what you find. Seems like a good opportunity for a fork.

ericdvb avatar Jun 06 '17 16:06 ericdvb

@ericdvb

So, in a nutshell, I got this to work pretty much with the hybrid way.

In my case, the child component passed in to Infinite is much more complex than the normal use case I think. I actually don't have just one list item rendering. Instead, I have nested children based on the list items. Picture something like this:

-Infinite -ContainerDiv -Grouped Div -Unordered List -Grouped Div -Unordered List -Grouped Div -Unordered List -/ContainerDiv -/Infinite

As you can tell, my "rows" can vary wildly in height. In addition, the documentation wasn't super clear to me. What I found out is that react-infinite considers a row to be the immediate child component of Infinite.

In my case, the immediate child component is my entire "ContainerDiv" (much like in your case, your table component). That means my elementHeight had to be this entire ContainerDiv's height. With that said, once I understood what was going on, the fix to my problems were easy.

I ended up setting in state a placeholder for this elementHeight to some arbitrary number (180 to be exact because my listitems are that exact height). Once things were rendered initially, the Infinite component fires the initial onInfiniteLoad() event. In my handler, after fetching my items from the server, I update state with the list. Things get re rendered automatically, since state was updated.

In my componentDidUpdate() function, I compare the previous state with the current state. If something is different with the length of my list that was stored in state, that means I know something was fetched (onInfiniteLoad() was called). If something was fetched, I go ahead and calculate the height of my ContainerDiv which houses all of my variable heights. I store this in state, which causes another render.

At this point, the Infinite component is reading the elementHeight variable from state, and things work just fine.

One thing to note is that you need to have that checking of prevState vs. currentState in componentDidUpdate or else you will just have an endless loop of rerenders.

My approach isn't the most ideal I don't think, but I can't think of anything else. Basically, I have to make 2 renders for every infinite scroll.

SmboBeast avatar Jun 07 '17 04:06 SmboBeast

@SmboBeast nice, sounds like it's working well.

I think it's generally not a good idea to call setState from componentDidUpdate for exactly the reason you described. To avoid rendering twice and having to compare props and nextProps for each infiniteLoad event, I'm doing my height calculation in componentWillReceiveProps, which it sounds like might also work in your case.

ericdvb avatar Jun 07 '17 05:06 ericdvb

@ericdvb

You know I'm totally with you. It looks like in your case you're dealing with props. How exactly?

I think in your case you are able to do so, because you have constant heights and can just do the calculations before rendering.

In my case, I actually need things to render, so I can grab the elementById and get the clientHeight off of the object. The componentWillReceiveProps happens too far up in the chain before I can access the DOM I believe, unless I'm wrong.

SmboBeast avatar Jun 07 '17 05:06 SmboBeast

@SmboBeast I think I'm misunderstanding, but you mentioned that your listItems have a height of 180, so it seems like you could multiply that by the number of listItems you have in componentWillReceiveProps's nextProps arg? I think what I'm probably missing is that your listItems aren't actually a constant height of 180, but I was thinking something like:

class myInfiniteContainer() {
  constructor(props) {
    super(props);
    this.state = {
      infinite: {
        elementHeight: 0,
        preloadBatchSize: 0,
      }  
    };
  }
  
  componentWillReceiveProps(nextProps) {
    const height = nextProps.listItems * 180;
    this.setState({
      infinite: {
        elementHeight: height,
        preloadBatchSize: height,
      }  
    });   
  }

  render() {
    <Infinite
      elementHeight={this.state.infinite.elementHeight}
      preloadBatchSize={this.state.infinite.preloadBatchSize}
      {...otherProps}
    >
      <ContainerDiv>
        {
          this.props.listItemsGroups.map(group => {
            return group.type === 'groupedDiv' ? <GroupedDiv /> : <ULWithChildren>{group.children}</ULWithChildren>;
          })
        }
      </ContainerDiv>
    </Infinite>
  }
}

ericdvb avatar Jun 07 '17 15:06 ericdvb

@ericdvb

Ahh, yes - it's probably my mistake. I set a default elementHeight to 180 to begin with, because I won't be able to calculate the dynamic height until after render.

With that said, if I don't provide an elementHeight at all, react-infinite blows up and doesn't like that. So I was forced to use a default number, which then quickly (after rerender), gets updated to the correct height, since I'm reading the elementHeight directly from my state parameter.

I think that should clear it up. I see your approach for sure, and it makes total sense. It's unfortunate I can't read from props in my case. :/

SmboBeast avatar Jun 07 '17 18:06 SmboBeast

@SmboBeast care to give an example of code? I have the exact same problem (dynamic height, have to use ReactMeasure to get the proper height of each child) but using setState causes me an infinite loop

matiasgarcia avatar Jul 27 '17 15:07 matiasgarcia

@SmboBeast although as you said it's not the cleanest solution, I had the exact same problem and this fixed it for me. Thanks!

ljle avatar Jan 23 '18 15:01 ljle

Hey @ericdvb Your solution works, but when you scroll does each row item get added to the div instead of just rendering the rows visible?

whiteb38 avatar Jan 31 '18 14:01 whiteb38

@ericdvb this solution seems to render all dom nodes and does not accomplish infinite scroll. Setting elementHeight that large renders all of the children.

ChrisWiles avatar Mar 28 '18 16:03 ChrisWiles

@ChrisWiles in order to get the windowing to work, i am no longer using a table, instead i am creating an array containing all of the elements for the page each with bootstrap column classes. So the page size will determine the number of items in the row, so using react-media i can change the elementheight see below:

<Media query="(min-width: 1200px)">
              {matches =>
                matches ? (
                  <Infinite elementHeight={44}
                           useWindowAsScrollContainer={true}
                           infiniteLoadBeginEdgeOffset={loadEdge}
                           onInfiniteLoad={this.handleInfiniteLoad}
                           isInfiniteLoading={this.state.isInfiniteLoading}
                           loadingSpinnerDelegate={this.elementInfiniteLoad()}
                           timeScrollStateLastsForAfterUserScrolls={1000}
                           preloadBatchSize={Infinite.containerHeightScaleFactor(2)}
                           >
                           {this.state.elements}
                  </Infinite>
) : (
                  <Media query="(min-width: 768px)">
                    {matches =>
                      matches ? (
                        <Infinite elementHeight={66}
                                 useWindowAsScrollContainer={true}
                                 infiniteLoadBeginEdgeOffset={loadEdge}
                                 onInfiniteLoad={this.handleInfiniteLoad}
                                 isInfiniteLoading={this.state.isInfiniteLoading}
                                 loadingSpinnerDelegate={this.elementInfiniteLoad()}
                                 timeScrollStateLastsForAfterUserScrolls={1000}
                                 preloadBatchSize={Infinite.containerHeightScaleFactor(2)}
                                 >
                                 {this.state.elements}
                        </Infinite>
                      ) : (
                        <Infinite elementHeight={132}
                                 useWindowAsScrollContainer={true}
                                 infiniteLoadBeginEdgeOffset={loadEdge}
                                 onInfiniteLoad={this.handleInfiniteLoad}
                                 isInfiniteLoading={this.state.isInfiniteLoading}
                                 loadingSpinnerDelegate={this.elementInfiniteLoad()}
                                 timeScrollStateLastsForAfterUserScrolls={1000}
                                 preloadBatchSize={Infinite.containerHeightScaleFactor(2)}
                                 >
                                 {this.state.elements}
                        </Infinite>
                      )
                  }
                  </Media>
                )
              }
            </Media>

whiteb38 avatar Mar 28 '18 18:03 whiteb38