How to react correctly when an item is mounted in React

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 refthen 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.

Go to our cases Get a free quote