Skip to content

Instantly share code, notes, and snippets.

@callmecavs
Created January 9, 2017 22:10
Show Gist options
  • Save callmecavs/0c056297c2902741f7afb6b1aa6bf5cb to your computer and use it in GitHub Desktop.
Save callmecavs/0c056297c2902741f7afb6b1aa6bf5cb to your computer and use it in GitHub Desktop.
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
@callmecavs
Copy link
Author

callmecavs commented Jan 9, 2017

When the component enters the viewport (for the first time) the inview state will change from false to true.

Note that the scroll handling logic is kept separate of the HoC. Events (scroll and resize) are binded/unbinded intelligently based on the number of components in the queue (if any). That action happens in componentDidMount and componentWillUnmount.

Prevents unnecessary updating in the shouldComponentUpdate lifecycle hook - depending on your use case, only updating if this.state.inview has changed might break some stuff.

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