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
- I can't set
new IntersectionObserver()
as the initial value inuseRef
. This is because it throws an error when runninggatsby 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.
}