0

Example on CodeSandbox


I am creating a website using Gatsby and am having trouble converting a class component, which uses IntersectionObserver, into a functional component that uses hooks. I am particularly looking for a solution that minimizes the amount of re-renders as possible.

My class component is shown below:

Parent component using React.Component

import React, { Component } from "react"
import ChildComponent from "../components/child"

class ParentComponent extends Component {
  observer = null

  componentDidMount() {
    this.observer = new IntersectionObserver(this.handleObserverEvent, {})
  }

  componentWillUnmount() {
    this.observer.disconnect()
    this.observer = null
  }

  handleObserverEvent = entries => {
    entries.forEach(entry => console.log(entry))
  }

  observeElement = ref => {
    if (this.observer) this.observer.observe(ref)
  }

  render() {
    const colors = ["red", "orange", "yellow", "green", "blue"]

    return (
      <div>
        {colors.map(color => (
          <ChildComponent
            key={color}
            color={color}
            observeElement={this.observeElement}
          />
        ))}
      </div>
    )
  }
}

export default ParentComponent

Child component

import React, { useRef, useEffect } from "react"

const ChildComponent = ({ color, observeElement }) => {
  const ref = useRef()

  useEffect(() => {
    if (ref.current) observeElement(ref.current)
  }, [observeElement])

  return (
    <div
      className={color}
      ref={ref}
      style={{
        width: "100vw",
        height: "100vh",
        background: color
      }}
    >
      {color}
    </div>
  )
}

export default ChildComponent

This current setup works well. I am able to observe ParentComponent's children using IntersectionObserver.

However, I need to convert this component into a functional component. I tried to implement this same logic using useRef, but cannot achieve the same result. In observeElement, observer.current is equal to null. My functional component is shown below.

Parent component using useRef

// Omitted imports/exports

const handleObserverEvent = entries => {
  entries.forEach(entry => console.log(entry))
}

const ParentComponent = () => {
  const observer = useRef(null)

  useEffect(() => {
    observer.current = new IntersectionObserver(handleObserverEvent, {})

    return () => {
      if (observer.current) observer.current.disconnect()
      observer.current = null
    }
  }, [])

  const observeElement = ref => {
    console.log(observer.current) // Logs null
    if (observer.current) observer.current.observe(ref)
  }

  const colors = ["red", "orange", "yellow", "green", "blue"]

  return (
    <div>
      {colors.map(color => (
        <ChildComponent
          key={color}
          color={color}
          observeElement={observeElement}
        />
      ))}
    </div>
  )
}

To combat this, I opted to use useState to trigger a re-render, which works. However, I am wondering if the same thing can be achieved using useRef and possibly some other hooks.

Parent component using useState

// Omitted imports/exports

const handleObserverEvent = entries => {
  // This successfully logs the entries
  entries.forEach(entry => console.log(entry))
}

const ParentComponent = () => {
  const [observer, setObserver] = useState(null)

  useEffect(() => {
    setObserver(new IntersectionObserver(handleObserverEvent, {}))

    return () => {
      if (observer) observer.disconnect()
    }
  }, [])

  const observeElement = ref => {
    if (observer) observer.observe(ref)
  }

  const colors = ["red", "orange", "yellow", "green", "blue"]

  return (
    <div>
      {colors.map(color => (
        <ChildComponent
          key={color}
          color={color}
          observeElement={observeElement}
        />
      ))}
    </div>
  )
}

Notes

  1. I can't set new IntersectionObserver() as the initial value in useRef. This is because it throws an error when running gatsby build. According to this Stack Overflow answer, IntersectionObserver is unavailable during Gatsby's build process since Gatsby uses server-side rendering.
const ParentComponent = () => {
  const observer = useRef(new IntersectionObserver())
  // Throws an error: IntersectionObserver is not defined.
}
Community
  • 1
  • 1
khan
  • 1,466
  • 8
  • 19

1 Answers1

0

The reason for your problem with useRef is pretty simple. You are triggering observeElement function from child's useEffect. and useEffect in child runs before the useEffect in parent so observeElement runs before the observer is actually initialised.

Now if you would have not been using Gatsby the solution would be to initialize observer within useRef itself.

However the best solution in the above case is to actually store the observer in state and trigger a re-render so that the observeElement function is called again from the child and it actually will now have the observer to work which is what you have already suggested in your post

Other solutions involve moving the ref of all children into the parent and triggering the observer from parent itself on each child but its quite hacky.

Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400