Skip to content

Instantly share code, notes, and snippets.

@gja
Last active June 7, 2021 11:12
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gja/81fa39e8c4ee4d707604810b61da3dfa to your computer and use it in GitHub Desktop.
Save gja/81fa39e8c4ee4d707604810b61da3dfa to your computer and use it in GitHub Desktop.
React Infinite Scroll with IntersectionObserver
const React = require("react");
// An item in the infinite scroll
class ScrollItem extends React.Component {
constructor(props) {
super(props);
this.state = {
minHeight: props.minHeight
}
}
render() {
return <div ref={node => this.node = node}
data-infinite-scroll={this.props.index}
style={{minHeight: this.state.minHeight}}>
{this.props.show && this.props.render(Object.assign({index: this.props.index}, this.props.data))}
</div>;
}
componentWillReceiveProps(nextProps) {
if(nextProps.show == false && this.props.show == true) {
this.setState({minHeight: this.node.clientHeight})
}
}
componentDidMount() {
this.props.observers.forEach(observer => observer && observer.observe(this.node));
}
componentWillUnmount() {
this.props.observers.forEach(observer => observer && observer.unobserve(this.node));
}
}
// When this becomes visible, we call loadMore()
class ScrollLoadMore extends React.Component {
render() {
return <div ref={node => this.node = node} data-infinite-scroll="load-more"/>;
}
componentDidMount() {
this.props.observers.forEach(observer => observer && observer.observe(this.node));
}
componentWillUnmount() {
this.props.observers.forEach(observer => observer && observer.unobserve(this.node));
}
}
// Basic Infinite Scroll, toggles showing items
class InfiniteScrollBase extends React.Component {
constructor(props) {
super(props);
this.state = {
visibleComponents: {0: true},
itemCount: 1
}
if(global.IntersectionObserver) {
this.loadObserver = new IntersectionObserver((x) => this.intersectionCallback(x), {
rootMargin: props.loadMargin || "100px 0px"
});
}
}
componentWillUnmount() {
this.loadObserver && this.loadObserver.disconnect();
}
intersectionCallback(entries) {
var visibleComponents = this.state.visibleComponents;
entries.forEach(entry => {
const item = entry.target.getAttribute("data-infinite-scroll");
if(item == 'load-more' && entry.isIntersecting) {
this.props.loadNext();
} else {
visibleComponents = Object.assign({}, visibleComponents, {[item]: entry.isIntersecting});
}
})
this.setState({visibleComponents: visibleComponents});
}
render() {
return <div>
{this.props.items.map((data, index) =>
<ScrollItem observers={this.props.observers.concat([this.loadObserver])}
key={index}
index={index}
show={this.state.visibleComponents[index]}
render={this.props.render}
data={data}
minHeight={this.props.minHeight || 50}/>)}
<ScrollLoadMore observers={[this.loadObserver]} />
</div>;
}
}
// Calls a callback when an item covers bottom 20% of the screen (to change URL)
function withFocusObserver(Component) {
return class WithFocusObserver extends React.Component {
constructor(props) {
super(props);
if(global.IntersectionObserver) {
this.focusObserver = new IntersectionObserver((x) => this.focusCallback(x), {
rootMargin: `-${100 - props.focusCallbackAt}% 0px -${props.focusCallbackAt}%`
})
}
}
componentWillUnmount() {
this.focusObserver && this.focusObserver.disconnect();
}
focusCallback(entries) {
entries.forEach(entry => {
const item = entry.target.getAttribute("data-infinite-scroll");
if(entry.isIntersecting) {
this.props.focusCallback(item)
}
});
}
render() {
return React.createElement(Component, Object.assign({}, this.props, {
observers: (this.props.observers || []).concat([this.focusObserver])
}))
}
}
}
const InfiniteScroll = withFocusObserver(InfiniteScrollBase);
class TestComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false,
items: [{headline: "story-0"}]
}
}
loadNext() {
if(this.state.loading)
return;
this.setState({loading: true});
setTimeout(() => this.setState({loading: false, items: this.state.items.concat([{headline: `random-${Math.random()}`},{headline: `random-${Math.random()}`},{headline: `random-${Math.random()}`},{headline: `random-${Math.random()}`},{headline: `random-${Math.random()}`}])}), 200)
}
render() {
return <InfiniteScroll render={(props) => <div className="scroll-component">{props.index} - {props.headline}</div>}
items={this.state.items}
minHeight={50}
loadNext={() => this.loadNext()}
loadMargin="200px 0px 100px"
focusCallbackAt={20}
focusCallback={(x) => console.log("Focussed Item", x)}/>
}
}
exports.TestComponent = TestComponent;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment