Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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;
like image 684
cbdeveloper Avatar asked May 14 '19 18:05

cbdeveloper


People also ask

How do you add ref in React hook?

A ref can be created in two ways- by the useRef hook or by the createRef function. useRef: The useRef is a hook that uses the same ref throughout. It saves its value between re-renders in a functional component and doesn't create a new instance of the ref for every re-render.

Can I use useContext in custom hook?

Create the useContextInside of this custom hook, we'll be using the useContext hook, that allows us to access both the theme and the setTheme function outside of this file. If useContext fails to create a context, it'll return undefined because we forgot the wrap our App or component in a ThemeProvider.

Can we use useState in custom hook?

Rather than litter your components with a bunch of useState calls to keep track of the state of an async function, you can use our custom hook which takes an async function as an input and returns the value , error , and status values we need to properly update our UI.

Can custom Hooks return component?

As seen in the code examples, using hooks to encapsulate shared logic is a good start, but we can keep exploring more. Inspired by the concept of “partial application”, we can try to return a component with props that have been partially bound from a custom hook.


Video Answer


1 Answers

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

like image 154
Shubham Khatri Avatar answered Nov 15 '22 07:11

Shubham Khatri