Created
January 9, 2017 22:10
-
-
Save callmecavs/0c056297c2902741f7afb6b1aa6bf5cb to your computer and use it in GitHub Desktop.
This file contains hidden or 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
import React, { Component, PropTypes } from 'react' | |
import ReactDOM from 'react-dom' | |
import throttle from 'lodash/throttle' | |
import { INTERVAL } from './constants.js' | |
let bound // are the event handlers bound? | |
let current // current scroll position, from top of document | |
let queue // array of React Components to check | |
const handler = event => { | |
// update current scroll position | |
current = window.scrollY || window.pageYOffset | |
// if no Components, exit early | |
if(!queue || !queue.length) { | |
return | |
} | |
// for each component | |
queue.forEach(comp => { | |
// get the DOM node | |
const node = ReactDOM.findDOMNode(comp) | |
// no node? exit early | |
// preserve default inview state of false | |
if(!node) { | |
return | |
} | |
// get top offset of node | |
// adjust for bounding rect being relative to viewport, not document | |
const offset = node.getBoundingClientRect().top + current | |
// update Component state | |
// components are only removed from the queue when unmounted | |
comp.updateState(offset >= current) | |
}) | |
} | |
const throttled = throttle(handler, INTERVAL) | |
const compose = Decorated => { | |
class Viewport extends Component { | |
state = { | |
inview: false | |
} | |
updateState = bool => { | |
this.setState({ inview: bool }) | |
} | |
// LIFECYCLE | |
componentDidMount = () => { | |
// lazily initialize queue | |
if(!queue) { | |
queue = [] | |
} | |
// push component onto the array | |
queue.push(this) | |
// if not bound already, bind event listeners | |
if(!bound) { | |
window.addEventListener('scroll', throttled) | |
window.addEventListener('resize', throttled) | |
bound = true | |
} | |
} | |
componentWillUnmount = () => { | |
// remove Component from array, if it exists | |
const index = queue.indexOf(this) | |
if(index !== -1) { | |
queue.splice(index, 1) | |
} | |
// if no more Components to check, unbind event listeners | |
if(bound && !queue.length) { | |
window.removeEventListener('scroll', throttled) | |
window.removeEventListener('resize', throttled) | |
bound = false | |
} | |
} | |
shouldComponentUpdate = (nextProps, nextState) => { | |
// only update if the inview status has changed | |
return nextState.inview !== this.state.inview | |
} | |
// RENDER | |
render = () => { | |
return ( | |
<Decorated | |
{ ...this.props } | |
inview={ this.state.inview } | |
/> | |
) | |
} | |
} | |
return Viewport | |
} | |
export default compose |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
When the component enters the viewport (for the first time) the
inview
state will change fromfalse
totrue
.Note that the scroll handling logic is kept separate of the HoC. Events (
scroll
andresize
) are binded/unbinded intelligently based on the number of components in the queue (if any). That action happens incomponentDidMount
andcomponentWillUnmount
.Prevents unnecessary updating in the
shouldComponentUpdate
lifecycle hook - depending on your use case, only updating ifthis.state.inview
has changed might break some stuff.