Search code examples
htmlreactjstypescriptref

Typing a custom useRef hook for any type of HTML element


I'm still a bit new to Typescript and my goal is to create a custom React.useRef hook in Typescript that returns a ref. I would like this ref to be typed for any HTML element and not just buttons only. I'm running into issues of typing this custom hook properly.

Here's my hook:

type CustomRefHook = (dependencies?: string | boolean | number[]) => React.MutableRefObject<HTMLButtonElement | null>;

const useCustomRefHook: CustomRefHook = (...dependencies) => {
  const ref = React.useRef<HTMLButtonElement | null>(null);

  useEffect(() => {
    // ... do stuff
  }, dependencies)

  return ref;
}

For now, the majority of my use cases for this hook would be for buttons but I would like to eventually expand this to any sort of HTML element. I've tried using HTMLElement instead of HTMLButtonElement , however I get a type error of:

Type 'MutableRefObject<HTMLElement | null>' is not assignable to type 'string | ((instance: HTMLButtonElement | null) => void) | RefObject<HTMLButtonElement> | null | undefined'.
  Type 'MutableRefObject<HTMLElement | null>' is not assignable to type 'RefObject<HTMLButtonElement>'.
    Types of property 'current' are incompatible.
      Type 'HTMLElement | null' is not assignable to type 'HTMLButtonElement | null'.

whenever I try to attach this ref to a button. Using HTMLButtonElement quiets the Typescript errors, but again I'd like this hook to be able to handle any type of HTML element.


Solution

  • The type of the ref needs to exactly match the DOM element that it is attached to, so you need to use a generic to specify the element type.

    Since useCustomRefHook is now a generic function, it makes more sense to declare the typings inline rather than as a separate type CustomRefHook.

    I have allowed this hook to accept any value of T, but you could use T extends HTMLElement if you just want it to apply to HTML elements. That might be important depending on the contents of "do stuff" inside your useEffect. If "do stuff" requires anything specific about T, then you need to make sure that T extends something with the required properties/methods.

    import React, { MutableRefObject, DependencyList, useEffect, useRef } from "react";
    
    const useCustomRefHook = <T extends any>( dependencies: DependencyList = [] ): MutableRefObject<T | null> => {
    
      const ref = useRef<T | null>(null);
    
      useEffect(() => {
        // ... do stuff
      }, dependencies);
    
      return ref;
    };
    

    The hook returns a MutableRefObject<T | null> based on the generic T. The value of T cannot possibly be inferred from the arguments, so you will need to specify the generic every time that you call it.

    const Test = () => {
      const ref = useCustomRefHook<HTMLButtonElement>();
    
      return <button ref={ref} />;
    };