Created
December 20, 2016 08:44
-
-
Save threepointone/0564095704d76757c631b891f634899a to your computer and use it in GitHub Desktop.
infinite scrolling pattern with react fiber (featuring intersection observers)
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
// 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.