DEV Community

neil-morgan
neil-morgan

Posted on

React Context and Provider to help handle scroll and viewport visibility events

I've run into a roadblock when putting together the below process.
I'm trying to create a context which will globally provide window properties to components so that the components can react to window changes (scroll, resize, etc).

I'm doing this so that rather than each component running their own set of event listeners. I only have to run one set and pass the results down.

So far the context adds the scroll and resize listeners, throttles them and makes the values available to the provider wrapper. All works fine, and the values are available in the wrapped component.

//WindowContext.js

import React, { Component, createContext } from "react"
import throttle from "lodash.throttle"

export const WindowContext = createContext()

class WindowContextProvider extends Component {
  constructor(props) {
    super(props)
    this.state = {
      windowScrollY: null,
      windowInnerHeight: null,
    }
    this.throttledFunction = throttle(this.updateState, 500)
  }

  componentDidMount() {
    window.addEventListener("scroll", this.throttledFunction, false)
    window.addEventListener("resize", this.throttledFunction, false)
  }
  componentWillUnmount() {
    window.removeEventListener("scroll", this.throttledFunction, false)
    window.removeEventListener("resize", this.throttledFunction, false)
  }

  updateState = () => {
    this.setState({
      windowScrollY: window.scrollY,
      windowInnerHeight: window.innerHeight,
    })
  }

  render() {
    return (
      <WindowContext.Provider value={{ ...this.state }}>
        {this.props.children}
      </WindowContext.Provider>
    )
  }
}

export default WindowContextProvider

The problem

In SomeComponent.js below you can see what it is what I am attempting. It has two fundamental problems.

I cannot (shouldn't) set state within render so I'm not sure how I can set isActive within the state to true (so that some ternary operator could utilise it). And the element coordinates I gather in the componentDidMount are static meaning I cant match them to scroll or inner height to determine if the element is visible.

//SomeComponent.js

import React from "react"
import ReactDOM from "react-dom"

import { WindowContext } from "../contexts/WindowContext"

class DevTools extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      isActive: false, //want to set isActive:true if 
      element: null,
    }
  }
  componentDidMount = () => {
    this.setState({
      element: ReactDOM.findDOMNode(this).getBoundingClientRect(),
    })
  }

  render() {
    return (
      <WindowContext.Consumer>
        {context => {
          //THE CRUX OF THE PROBLEM
          const { windowScrollY, windowInnerHeight } = context
          console.log(windowScrollY) //tracked
          console.log(windowInnerHeight) //tracked
          console.log(this.state.element) //not tracked
          //this.state.element is currently static as it is simply set on mount, needs to be tracked
          //adding an event listener would defeat the point of creating the context

          //Below not an option, cannot set state within render, not sure how else to manage toggling when the element is visible
          handleWindowChange = () => { 
            if (this.state.element.top + 100 < windowInnerHeight && this.state.element.bottom >= 0) {
              this.setState({
                isActive: true,
              })
            }
          }
          return (
          //some div with a ternary operator

I'm trying to come up with a solution which both GLOBALLY tracks the event listeners (scroll, resize) but also tracks the position/visibility of any component the provider wraps.

So maybe something in which I can pass all components that the context provider wraps up to the context state and handle matching their visibility there instead. But I'm struggling to wrap my head around where to start

Top comments (1)

Collapse
 
thiagoaslima profile image
Thiago Lima

Its a bit old, so maybe you already found a solution. My suggestion is take a look on IntersectionObserver API.

You can register all components you want to track and pass the setter, following an Observer pattern