Skip to content

Instantly share code, notes, and snippets.

@christianscott
Last active May 5, 2017 02:09
Show Gist options
  • Save christianscott/8874de729339ff5edc94755859a221f7 to your computer and use it in GitHub Desktop.
Save christianscott/8874de729339ff5edc94755859a221f7 to your computer and use it in GitHub Desktop.
Virtualised grid using ReactJS and RxJS
/*
* Virtualised grid using ReactJS and RxJS.
*
* Squares in the grid have the class `visible` only when they are inside the viewport.
*
* When the container has not been scrolled for 50ms, Grid.invalidate() is
* called and the visibility of each of the squares is checked using getBoudingClientRect().
*
* It is necessary to debounce the stream, otherwise this component will be horribly slow.
*
* Not particularly useful in this form, but could be used as the basis
* for more complex components that need a performance boost.
*
* Possible improvements:
* 1. Currently, the invalidate function has complexity of O(n) due to the fact that each of
* the items has to be checked one-by-one to test for visibility.
*
* However, if we know the dimensions of the container, the dimensions of the items and the
* current scroll height of the container, the indices of the visible items could be calculated
* in O(1) time.
*/
import React, { Component } from 'react';
import { render } from 'react-dom';
import { Observable } from 'rxjs';
class Grid extends Component {
constructor(props) {
super(props);
// use an array of objects
const items = new Array(1000).fill({ visible: false });
this.state = { items };
}
componentDidMount() {
// must do this at least once, otherwise all squares will be invisible
this.invalidate(this.container.getBoundingClientRect());
// this.container is created once the component has mounted via the ref prop
const containerScrollStream$ = Observable.fromEvent(this.container, 'scroll')
.debounceTime(50)
.map(ev => ev.target.getBoundingClientRect()); // emit the container dimensions
// when the stream emits an event, call this.invalidate on the value emitted
containerScrollStream$.subscribe(
val => this.invalidate(val),
err => console.error(err)
);
}
invalidate(containerClientRect) {
// create a new array of items with updated visibility
const updatedItems = this.state.items.map((item, i) => {
const visible = this.isInsideViewport(
this.refs[`item_${i}`].getBoundingClientRect(),
containerClientRect
);
return { visible };
});
this.setState({ items: updatedItems });
}
renderItems(items) {
return items.map((item, i) =>
<div
className={`item ${item.visible ? 'visible' : ''}`}
key={i}
ref={`item_${i}`}
>
I'm visible!
</div>
);
}
isInsideViewport(itemClientRect, containerClientRect) {
const TOLERANCE = 200; // how many px either side of the container should items be rendered?
return (
itemClientRect.bottom >= (containerClientRect.top - TOLERANCE) &&
itemClientRect.top <= (containerClientRect.bottom + TOLERANCE)
);
}
render() {
const gridItems = this.renderItems(this.state.items);
return (
<div
className="container"
ref={container => this.container = container}
>
{ gridItems }
</div>
);
}
}
render(
<Grid />,
document.querySelector('#root')
);
@christianscott
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment