Last active
May 5, 2017 02:09
-
-
Save christianscott/8874de729339ff5edc94755859a221f7 to your computer and use it in GitHub Desktop.
Virtualised grid using ReactJS and RxJS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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') | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Try it here