2

I have a particularly interesting problem with respect to infinite scrolling using react.js.

The goal here is to make sure no matter how large a table becomes, we only ever let render() return a fixed subset of all rows.

We will let lowerVisualBound and upperVisualBound denote the subset of rows to render() onto the DOM.

Note these bounds are set to be larger than the viewport, causing a scrollbar to appear.

We will modify lowerVisualBound and upperVisualBound as the user scrolls.

Here, we further denote

  • height as the height of the visible portion of the table
  • totalHeight as the height of the entire table (that includes all rows between lowerVisualBound and upperVisualBound
  • scrollTop and state.lastScrollTop as the current and previous scroll top respectively

The Following Snippet Kind of Does the Trick - Except the scrollbar itself does not change position after additional data has been loaded (i.e. upper or lower VisualBound reset). This causes the user's view of the data to jump.

    const rowDisplayBoundry = 2 * this.props.pageSize;
    if (scrollTop < this.state.lastScrollTop && scrollTop <= 0.0 * totalHeight) {
        // up scroll limit triggered
        newState.lowerVisualBound = Math.max(this.state.lowerVisualBound - this.props.pageSize, 0);
        newState.upperVisualBound = newState.lowerVisualBound + rowDisplayBoundry;
    } else if (scrollTop > this.state.lastScrollTop && (scrollTop + height) > totalHeight) {
        // down scroll limit triggered
        newState.upperVisualBound = this.state.upperVisualBound + this.props.pageSize;
        newState.lowerVisualBound = newState.upperVisualBound - rowDisplayBoundry;
    }
// TODO now what do we set scrollTop to, post these mutations? (presumably using a setTimeout())

Can anyone suggest an algorithm to compute a new scrollTop such that changing the visual bound preserve the user's view?

Note I believe this should be theoretically possible because the # of rows between upper & lower visual bound is set to be > what can be displayed in the viewport. Therefore, after each mutation in those bounds, the user does not lose any rows that he was viewing immediately before the mutation. It is only a matter of computing the correct location the scrollbar post-mutation.

2
  • What about rendering a spacer above and below the visible portion, recording scrollTop in willupdate, and setting it to that in didupdate? Are you able to calculate the total height of the table if all items were rendered? This also lets the user seek by dragging the scroll bar, and let's them see if e.g. they're half way through the table. Commented Jun 28, 2015 at 13:43
  • that is a good point, I will contemplate that. It is true we can compute the total height if all items are rendered, assuming the unrendered rows have the same average height as the rendered row. the approach below indeed doesn't allow them to see total progress (sufficient for my use case, but maybe not generally applicable to all) Commented Jun 28, 2015 at 14:49

1 Answer 1

1

The following appears to have worked ... though not sure if there are corner cases where it doesn't (please excuse the rather liberal use of jQuery selector for this demostration)

handleScroll: function (e) {
    const $target = $(e.target);
    const scrollTop = $target.scrollTop();
    const height = $target.height();
    const totalHeight = $target.find("tbody").height();
    const avgRowHeight = totalHeight / (this.state.upperVisualBound - this.state.lowerVisualBound);
    /**
     * always update lastScrollTop on scroll event - it helps us determine
     * whether the next scroll event is up or down
     */
    var newState = {lastScrollTop: scrollTop};

    /**
     * we determine the correct display boundaries by keeping the distance between lower and upper visual bound
     * to some constant multiple of pageSize
     */
    const rowDisplayBoundry = 2 * this.props.pageSize;
    if (scrollTop < this.state.lastScrollTop && scrollTop <= 0) {
        // up scroll limit triggered
        newState.lowerVisualBound = Math.max(this.state.lowerVisualBound - this.props.pageSize, 0);
        newState.upperVisualBound = newState.lowerVisualBound + rowDisplayBoundry;
        // if top most rows reached, do nothing, otherwise reset scrollTop to preserve current view
        if (!(newState.lowerVisualBound === 0))
            setTimeout(function () {
                $target.scrollTop(Math.max(scrollTop + this.props.pageSize * avgRowHeight, 0));
            }.bind(this));

    } else if (scrollTop > this.state.lastScrollTop && (scrollTop + height) >= totalHeight) {
        // down scroll limit triggered
        newState.upperVisualBound = this.state.upperVisualBound + this.props.pageSize;
        newState.lowerVisualBound = newState.upperVisualBound - rowDisplayBoundry;
        setTimeout(function () {
            // TODO ensure that new scrollTop doesn't trigger another load event
            // TODO ensure this computationally NOT through flagging variables
            $target.scrollTop(scrollTop - this.props.pageSize * avgRowHeight);
        }.bind(this));
    }
    this.setState(newState);
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.