cbdeveloper
cbdeveloper

Reputation: 31415

Passing a ref to an HTML element to a custom hook

Imagine I have a custom hook that I'll use to add a click event listener into an HTML element.

I create the ref with const buttonRef = useRef(null); , so the value on first render is null. The ref value is only assigned in the final of the render method, at a point where my custom hook has already been called.

Therefore, on first render, my custom hook doesn't have anything to add an event listener to.

I end up having to update the component before my custom hook can run for the second time and finally add the event listener to the element. And I get the following behavior:

enter image description here

Sandbox with example

QUESTION:

How to get around this? Do I really need to force update my component in order to send a ref to a custom hook? Since I only can call hooks and custom hooks at top-level (rules of hooks), something like the following it's not allowed.

useEffect(() => {
  useMyHook();
});

App.js

function App() {
  const buttonRef = useRef(null);
  const hookValue = useMyHook(buttonRef.current);
  const [forceUpdate, setForceUpdate] = useState(false);

  return (
    <div>
      <button onClick={() => setForceUpdate(prevState => !prevState)}>
        Update Component
      </button>
      <button ref={buttonRef}>Update Hook</button>
      {"This is hook returned value: " + hookValue}
    </div>
  );
}

useMyHook.js (custom hook)

import { useEffect, useState } from "react";

function useMyHook(element) {
  const [myHookState, setMyHookState] = useState(0);

  console.log("Inside useMyhook...");
  console.log("This is the element received: " + element);

  useEffect(() => {
    console.log("Inside useMyhook useEffect...");

    function onClick() {
      setMyHookState(prevState => prevState + 1);
    }

    if (element !== null) {
      element.addEventListener("click", onClick);
    }
    return () => {
      console.log("Inside useMyhook useEffect return...");
      if (element !== null) {
        element.removeEventListener("click", onClick);
      }
    };
  });

  return myHookState;
}

export default useMyHook;

Upvotes: 17

Views: 18486

Answers (1)

Shubham Khatri
Shubham Khatri

Reputation: 281774

The solution is pretty trivial, you just need to pass on the ref to the custom hook instead of ref.current since, ref.current is mutated at its original reference when the ref is assigned to the DOM and the useEffect in your custom hook is triggered post the first render, so buttonRef.current will reference the DOM node

useMyHook.js

import { useEffect, useState } from "react";

function useMyHook(refEl) {
  const [myHookState, setMyHookState] = useState(0);

  console.log("Inside useMyhook...");
  console.log("This is the element received: ", refEl);

  useEffect(() => {
    const element = refEl.current;
    console.log("Inside useMyhook useEffect...");

    function onClick() {
      console.log("click");
      setMyHookState(prevState => prevState + 1);
    }
    console.log(element);
    if (element !== null) {
      element.addEventListener("click", onClick);
    }
    return () => {
      console.log("Inside useMyhook useEffect return...");
      if (element !== null) {
        element.removeEventListener("click", onClick);
      }
    };
  }, []);

  return myHookState;
}

export default useMyHook;

index.js

function App() {
  const buttonRef = useRef(null);
  const hookValue = useMyHook(buttonRef);
  const [forceUpdate, setForceUpdate] = useState(false);

  return (
    <div>
      <button onClick={() => setForceUpdate(prevState => !prevState)}>
        Update Component
      </button>
      <button ref={buttonRef}>Update Hook</button>
      {"This is hook returned value: " + hookValue}
    </div>
  );
}

Working demo

Upvotes: 26

Related Questions