Skip to content

Refs Revisited

Earlier in this course, we built a custom useIsOnscreen hook.

There's something peculiar about it, though. When we really think about it, it's sorta surprising that it works at all!

Let's dig a bit deeper.

Video Summary

(This is a difficult video to translate into text, so this summary will stay pretty high-level; check out the video for the full story!)

The useIsOnscreen hook allows us to pass in an element ref, and find out whether it's within the viewport:

import useIsOnscreen from './hooks/use-is-onscreen.js';
function App() {
// Create a ref that holds a DOM node:
// { current: <div> }
const elementRef = React.useRef();
// Pass it into `useIsOnscreen`, get back
// a boolean value.
const isOnscreen = useIsOnscreen(elementRef);
return (
<>
<header>
Red box visible: {isOnscreen ? 'YES' : 'NO'}
</header>
<div className="wrapper">
{/* Capture a reference */}
<div ref={elementRef} className="red box" />
</div>
</>
);
}

The useIsOffscreen hook then uses that ref to set up an IntersectionObserver:

function useIsOnscreen(elementRef) {
const [isOnscreen, setIsOnscreen] = React.useState(false);
React.useEffect(() => {
const observer = new IntersectionObserver((entries) => {
const [entry] = entries;
setIsOnscreen(entry.isIntersecting);
});
observer.observe(elementRef.current);
return () => {
observer.disconnect();
};
}, [elementRef]);
return isOnscreen;
}

Some students have thought it would be nicer to pass the element directly, rather than the entire ref object:

// App.js
- const isOnscreen = useIsOnscreen(elementRef);
+ const isOnscreen = useIsOnscreen(elementRef.current);
// In useIsOnscreen.js
- observer.observe(elementRef.current);
+ observer.observe(elementRef);

Curiously, this leads to an error:

Failed to execute 'observe' on 'IntersectionObserver': parameter 1 is not of type 'Element'.

To understand why this happens, we need to walk through how this works, step by step. The order of operations here is super important. Here's what happens:

  1. We create a new elementRef ref, which gets initialized to undefined.
    { current: undefined }
  2. We call the useIsOnscreen function, and pass this object in. The effect is scheduled, but doesn't run yet.
  3. We create a bunch of React elements, and return them from the App component
  4. As we saw in the “Core React Loop” lesson, React will then produce a bunch of real DOM nodes, using the React elements as a guide.
  5. After the DOM nodes have been created, our reference is captured, updating the elementRef object.
    { current: <div> }
  6. The effect we scheduled earlier runs, setting up the IntersectionObserver.

This component is only rendered once, and so the useIsOnscreen function is only called once. At the moment it's called, we don't yet have a reference to the DOM node, because that DOM node doesn't even exist yet!

But, because refs work by mutating an object, that doesn't matter; by the time the effect runs, that object will have been edited to include the reference.

By contrast, when we try to pass the element directly (useIsOnscreen(elementRef.current)), we pluck the value from the object. That value is undefined, at the time.

Depending on your comfort level with JS, this either makes perfect sense, or it's perfectly bewildering. In the video, I explain it in a couple other ways:

  1. We use the "RAM Visualization" from earlier to see how the ref object is stored in memory.
  2. We take React out of the equation, and see how this same issue can be demonstrated in pure JS.

Here's the sandbox from the video:

Code Playground

import React from 'react';

import useIsOnscreen from './hooks/use-is-onscreen.js';

function App() {
const elementRef = React.useRef();

const isOnscreen = useIsOnscreen(elementRef);

return (
<>
<header>
Red box visible: {isOnscreen ? 'YES' : 'NO'}
</header>
<div className="wrapper">
<div ref={elementRef} className="red box" />
</div>
</>
);
}

export default App;

And here's the memory visualization:

Computer Memory

refs
Snapshot #1