I have a custom React Hook that watches for a click outside of a specific element. It works just fine, but I am having trouble making TypeScript happy in a few places.
App.js
import { useRef, useCallback } from "react";
import useClick from "./useClick";
export default function App() {
const asideRef = useRef(null);
const handleStuff = useCallback(() => {
console.log("a click outside of the sidebar occurred.");
}, []);
useClick(asideRef, handleStuff);
return (
<div className="App">
<aside ref={asideRef}>
<nav>
<ul></ul>
</nav>
</aside>
</div>
);
}
useClick.js
import React, { useEffect } from "react";
const useClick = (ref: React.MutableRefObject<HTMLElement>, cb: () => void) => {
useEffect(() => {
const checkClick = (e: React.MouseEvent): void => {
if (ref.current && !ref.current.contains(e.target as Node)) {
cb();
}
};
document.addEventListener("click", checkClick);
return () => {
document.removeEventListener("click", checkClick);
};
}, [ref, cb]);
};
export default useClick;
The first problem area is in App.js
, where the useClick
hook is called. TypeScript complains about the first parameter passed to useClick
and gives a message of:
Argument of type 'MutableRefObject<null>' is not assignable to parameter of type 'MutableRefObject<HTMLElement>'.
Type 'null' is not assignable to type 'HTMLElement'.ts(2345)
I know this has something to do with me setting the initial value of the ref to null, and the argument for the ref in useClick
being annotated as React.MutableRefObject<HTMLElement>
. I just don't know how to fix it.
The second problem area is in useClick.js
, where the event listeners are added and removed. TypeScript seems to have a problem with my checkClick
function. The error is so long that I have no choice but to show a photo of it below.
If anyone has any idea's on how to fix these two issues, so TypeScript will be happy, the help would be greatly appreciated.
CodePudding user response:
Hi Dan: I'll start with what needs to change, then explain why below. (I also renamed your TypeScript file extensions to reflect their content.)
useClick.ts
Before:
// ...
const useClick = (ref: React.MutableRefObject<HTMLElement>, cb: () => void) => {
useEffect(() => {
const checkClick = (e: React.MouseEvent): void => {
// ...
After:
// ...
const useClick = (ref: React.RefObject<HTMLElement>, cb: () => void) => {
useEffect(() => {
const checkClick = (e: MouseEvent): void => {
// ...
App.tsx
Before:
export default function App() {
const asideRef = useRef(null);
After:
export default function App() {
const asideRef = useRef<HTMLElement>(null);
First, let's address the checkClick
function: This function signature should be assignable to the EventListener
type in lib.dom.d.ts
:
interface EventListener {
(evt: Event): void;
}
Because your parameter is React.MouseEvent
, which does not extend the native Event
, type, it is incompatible. Just use the native MouseEvent
type instead, since you're adding the listener to the document (which has nothing to do with React anyway.)
Next, let's look at the function signature for useClick
:
You might want to reference the type definitions for useRef
while reading this part:
The first parameter is the only one that needs to change, and it needs to change from a MutableRefObject
to a RefObject
. The difference between these can be subtle, but there are some great resources you can read to learn more about them. Here's one, for example:
https://dev.to/wojciechmatuszewski/mutable-and-immutable-useref-semantics-with-react-typescript-30c9
When creating a ref that you pass to React to use for an element reference, you should provide null
as the initial argument to useRef
(which you did) and also provide a generic type annotation for the type of value it should hold (in this case it's HTMLElement
). When using a ref for a DOM element, React expects that you will never mutate this ref since it's being controlled by React, so the type that is returned by useRef
in this case is simply a RefObject
. Therefore, that's what you should use for the first parameter of useClick
.