Reputation: 31415
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:
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
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>
);
}
Upvotes: 26