How to react correctly when an item is mounted in React
If you want to react to mounting a React Element in your home, you might be tempted to use useRef
in order to obtain references and with useEffect
to respond to mounting and unmounting. But it won’t work. This is because when the component is installed (or removed), the ref
we assign the result useRef
, the callback will not run and there will be no re-rendering.
You’d even get a warning from eslint react-hooks. Administration ref
, nor ref.current
as dependencies in useEffect
they won’t trigger the callback.
See for yourself:
import React, { useEffect, useRef, useState } from "react";
import "./styles.css";
import catImageUrl from "./cat.png";
/*
for more info see:
https://medium.com/welldone-software/usecallback-might-be-what-you-meant-by-useref-useeffect-773bc0278ae
*/
export default function App() {
const [count, setCount] = useState(1);
const shouldShowImageOfCat = count % 3 === 0;
const [catInfo, setCatInfo] = useState(false);
// notice how none of the deps of useEffect
// manages to trigger the hook in time
const catImageRef = useRef();
useEffect(() => {
console.log(catImageRef.current);
setCatInfo(catImageRef.current?.getBoundingClientRect());
// notice the warning below
}, [catImageRef, catImageRef.current]);
return (
<div className="App">
<h1>useEffect & useRef vs useCallback</h1>
<p>
An image of a cat would appear on every 3rd render.
<br />
<br />
Would our hook be able to make the emoji see it?
<br />
<br />
{catInfo ? "😂" : "😩"} - I {catInfo ? "" : "don't"} see the cat 🐈
{catInfo ? `, it's height is ${catInfo.height}` : ""}!
</p>
<input disabled value={`render #${count}`} />
<button onClick={() => setCount((c) => c + 1)}>next render</button>
<br />
{shouldShowImageOfCat ? (
<img
ref={catImageRef}
src={catImageUrl}
alt="cat"
width="50%"
style={{ padding: 10 }}
/>
) : (
""
)}
</div>
);
}
Above the code in which useRef
as a dependency useEffect
it is not able to run it on time. Here’s the link to the sandbox.
So what can we do?
useCallback
Here is a link to a snippet of the official Reacta documentation that treats useCallback
Here we can rely on passing the usual function packed in useCallback
to ref
and respond to the last reference to the node in the house that will be returned.
Try it yourself:
import React, { useCallback, useState } from "react";
import "./styles.css";
import catImageUrl from "./cat.png";
/*
for more info see:
https://medium.com/welldone-software/usecallback-might-be-what-you-meant-by-useref-useeffect-773bc0278ae
*/
export default function App() {
const [count, setCount] = useState(1);
const shouldShowImageOfCat = count % 3 === 0;
const [catInfo, setCatInfo] = useState(false);
// notice how this is a useCallback
// that's used as the "ref" of the image below
const catImageRef = useCallback((catImageNode) => {
console.log(catImageNode);
setCatInfo(catImageNode?.getBoundingClientRect());
}, []);
return (
<div className="App">
<h1>useEffect & useRef vs useCallback</h1>
<p>
An image of a cat would appear on every 3rd render.
<br />
<br />
Would our hook be able to make the emoji see it?
<br />
<br />
{catInfo ? "😂" : "😩"} - I {catInfo ? "" : "don't"} see the cat 🐈
{catInfo ? `, it's height is ${catInfo.height}` : ""}!
</p>
<input disabled value={`render #${count}`} />
<button onClick={() => setCount((c) => c + 1)}>next render</button>
<br />
{shouldShowImageOfCat ? (
<img
ref={catImageRef}
src={catImageUrl}
alt="cat"
width="50%"
style={{ padding: 10 }}
/>
) : (
""
)}
</div>
);
}
Code in which useCallback
it was used as ref
and detects rendering on time. Here’s the link to the sandbox.
Reservation: function z ref
it will be triggered when you mount and unmount items, even when you first mount them, and even if the unmount is the result of a parent component being detached.
Reservation 2: make sure you wrap the ref callbacks in useCallback
.
If without the above we call render
from callback ref
then it will run again with null
, leading to an infinite loop caused by how React works inside.
You can experiment with that. Try removing usecallback from your code and see what it looks like here.
This pattern can be used in many ways:
useState
Because useState
this is a function that does not change between renders, so you can use it as ref
. In this case, the entire node will be preserved in its own state.
When the state changes, it starts re-render, which makes it usable in rendering results and as a dependency useEffect
:
const [node, setRef] = useState(null);
useEffect(() => {
if (!node) {
console.log('unmounted!');
return null;
}
console.log('mounted');
const fn = e => console.log(e);
node.addEventListener('mousedown', fn);
return () => node.removeEventListener('mousedown', fn);
}, [node])
// <div ref={setRef}....
useStateRef
Access to the house is expensive, so we aim not to do it too often. If you do not need the whole node as in the previous hook, it is best if you save only part of it in the state:
// the hook
function useStateRef(processNode) {
const [node, setNode] = useState(null);
const setRef = useCallback(newNode => {
setNode(processNode(newNode));
}, [processNode]);
return [node, setRef];
}
// how it's used
const [clientHeight, setRef] = useStateRef(node => (node?.clientHeight || 0));
useEffect(() => {
console.log(`the new clientHeight is: ${clientHeight}`);
}, [clientHeight])
// <div ref={setRef}....
// <div>the current height is: {clientHeight}</div>
As you can see, we get access to Dom only when the element to which we pass the ref has been mounted, and in the state we have only clientheight.
useRefWithCallback
Sometimes, however, you can do without running re-renders when you mount and unmount items that we use ref
.
The following hook does not save the node in the state. Instead of using state, hook responds directly to mount and unmount so that no re-renders are triggered.
// the hook
function useRefWithCallback(onMount, onUnmount) {
const nodeRef = useRef(null);
const setRef = useCallback(node => {
if (nodeRef.current) {
onUnmount(nodeRef.current);
}
nodeRef.current = node;
if (nodeRef.current) {
onMount(nodeRef.current);
}
}, [onMount, onUnmount]);
return setRef;
}
const onMouseDown = useCallback(e => console.log('hi!', e.target.clientHeight), []);
const setDivRef = useRefWithCallback(
node => node.addEventListener("mousedown", onMouseDown),
node => node.removeEventListener("mousedown", onMouseDown)
);
// <div ref={setDivRef}
Finally, you will be able to understand the principle of using useCallback
as a ref in the element. I think you’ll have your own ideas here, too.
You can read the original English text here.