Annotating Custom React Hook With TypeScript

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.

enter image description here

1 answer

  • answered 2021-10-24 18:32 jsejcksn

    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:

    https://github.com/DefinitelyTyped/DefinitelyTyped/blob/5149827c1f97541dd69d950039b83ace68e119e6/types/react/index.d.ts#L1025-L1063

    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.

How many English words
do you know?
Test your English vocabulary size, and measure
how many words do you know
Online Test
Powered by Examplum