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 |
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
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?