I am writing a custom hook that accepts a ref
. I would like to attach an event listener to this DOM node and remove the listener inside a cleanup function.
I started with something like this:
useIsFocused.ts
function useIsFocused(elem: React.MutableRefObject<HTMLElement | null>) {
const [hasFocus, setHasFocus] = useState(false);
useEffect(() => {
const watched = elem.current;
function setFocus() {
setHasFocus(true);
}
function removeFocus() {
setHasFocus(false);
}
if (watched) {
// set initial value
if (watched === document.activeElement) {
setHasFocus(true);
}
// watch
watched.addEventListener('focusin', setFocus);
watched.addEventListener('focusout', removeFocus);
// cleanup
return () => {
watched.removeEventListener('focusin', setFocus);
watched.removeEventListener('focusout', removeFocus);
};
}
}, [elem]);
}
That worked at first. However, once we started conditionally rendering the input elem, it stopped working. As the DOM node is destroyed and re-created, the handlers are not being cleaned up or re-attaching to the new node. After some digging, I realized it is because of the way refs work, since they are not part of re-renders. Of course, the useEffect
is not triggering if the ref.current
value changes. Therefore, my hook isn’t responding to the changes in the DOM node.
Ideally, I’d like to use it like this:
App.tsx
function App() {
const inputRef = useRef<HTMLInputElement | null>(null);
const inputIsFocused = useIsFocused(inputRef);
const [showInput, setShowInput] = useState(true);
return (
<div>
{ showInput &&
<input type="text" value={search} ref={inputRef} />
}
<p>Is focused? {JSON.stringify(inputisFocused)} </p>
<button onClick={()=> setShowInput((e) => !e)}>Toggle input</button>
</div>
);
}
My question is, what is the proper way to make a reusable, custom hook that cleanly attaches a listener to a DOM node, reacts to changes (such as mounting/unmounting the node) and handles cleanup? Emphasis on a clean DX implementation for the implementing component.
Related questions
- Detecting which input is focused React hooks (this comes closest but attaches the listener to the window using the window
blur
andfocus
events with capturing) - How to rerender when refs change (need to encapsulate in a hook)
2
Answers
As you already mentioned,
ref
s do not trigger re-render. However, applying aref
to a component (or HTML element) will update theref
on each re-render (asymptotically). So…. lets match the update cycle instead.👆 Apply the events on every re-render by removing the dependency array on
useEffect
Bonus tip why don’t you make the hook return a tupple
[refObject, hasFocus]
. Since you are in a custom hook, you can create a ref object within the hookI’d suggest staying away from
useRef
anduseEffect
as much as possible.Example 1:
useState
onlyExample 2: with event listeners wo
useRef
(upon request)Switching
useRef
touseState
to trigger re-render to triggeruseEffect
to add event listeners wheninputRef
is assigned.useRef
doesn’t provide re-render capability by design.Example 3: with event listeners w
useRef
(upon request)resetCounter
update triggers re-render which triggersuseEffect
which adds new event listeners