Skip to content

Instantly share code, notes, and snippets.

@threepointone
Created December 20, 2016 08:44
Show Gist options
  • Save threepointone/0564095704d76757c631b891f634899a to your computer and use it in GitHub Desktop.
Save threepointone/0564095704d76757c631b891f634899a to your computer and use it in GitHub Desktop.
infinite scrolling pattern with react fiber (featuring intersection observers)
// inifinite scrolling of content without extra wrappers
const { render, findDOMNode } = ReactDOMFiber
class App extends React.Component {
render() {
// wrap the root element with an Intersection Observer, exposing .observe for children
return <Intersection>
<div style={{ height: 200, overflow: 'auto' }}>
<Page offset={0} count={10} />
</div>
</Intersection>
}
}
class Page extends React.Component {
static contextTypes = {
observe: React.PropTypes.func
}
state = {
next: false
}
componentDidMount() {
// load more content when this element comes into view
this.context.observe(this.loadMore, () => this.setState({ next: true }))
}
render() {
let { count, offset } = this.props
return [
times(count, i =>
<div>
article {this.props.offset + i}
</div>),
this.state.next ?
<Page offset={offset + count} count={count}/> :
<div ref={x => this.loadMore = x || this.loadMore }> load more ...</div>
]
}
}
class Intersection extends React.Component {
static childContextTypes = {
observe: React.PropTypes.func
}
getChildContext(){
return {
observe: this.observe
}
}
elements = new Map()
elementBuffer = []
observe = (element, callback) => {
this.elements.set(element, callback)
// this funny bit to handle react's lifecycle order
if(!this.observer){
this.elementBuffer.push(element)
}
else {
this.observer.observe(element)
}
}
onIntersect = (entries, observer) => {
entries.forEach(entry => this.elements.get(entry.target)(entry))
}
componentDidMount(){
this.observer = new IntersectionObserver(this.onIntersect, {
root: findDOMNode(this),
rootMargin: '0px',
threshold: 0.25
})
this.elementBuffer.forEach(element => this.observer.observe(element))
this.elementBuffer = []
}
render() {
return this.props.children
}
}
function times(n, fn){
let arr = []
for(let i=0; i< n; i++){
arr.push(fn(i))
}
return arr
}
render(<App/>, window.app)
// homework -
// reclaim memory by removing dom nodes from the top without jitter
// go both ways; start from the middle and scroll up
// cleanup element handlers after loading
@axemclion
Copy link

Do you see any performance difference between stack and fiber due to intersection observers ? As I read the code, the extra nodes are only rendered when they are visible, so the amount of rendering may be similar in stack and fiber, right ?

I thought Intersection Observers could be used to set priorities to render off-screen elements later. Would that be more performant?

@threepointone
Copy link
Author

I don't think there's any perf difference per se, what's interesting is that this model isn't possible at all with the stack renderer (because of array returns). Wrapping the setState with a deferredUpdate might benefit scroll perf, tho I haven't tried it out.

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