Search code examples
reactjstypescriptreact-forwardref

How to specify that forwardRef is optional in a React component?


CodeSandbox Example for this issue.

I have defined an icon library that exports several icons. Each Icon forwards a ref to its internal svg. Here's an example:

type ReactIconProps = SVGProps<SVGSVGElement>;

function CheckIconInner(props: ReactIconProps, ref: Ref<SVGSVGElement>) {
  return (
    <svg
      fill="none"
      height="1em"
      ref={ref}
      stroke="currentColor"
      viewBox="0 0 24 24"
      width="1em"
      xmlns="http://www.w3.org/2000/svg"
      {...props}
    >
      <path
        d="M20 6L9 17L4 12"
        strokeLinecap="round"
        strokeLinejoin="round"
        strokeWidth={2}
      />
    </svg>
  );
}
const CheckIcon = forwardRef(CheckIconInner);

I want to use these icons as a prop in a button component so that the button can include an icon. See below:

type ReactButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;

export interface ButtonProps extends ReactButtonProps {
  Icon?: ComponentType<ReactIconProps>;
}

function Button({ children, Icon, ...props }: ButtonProps) {
  return (
    <button {...props}>
      {Icon !== undefined && <Icon />}
      {children}
    </button>
  );
}

export default function App() {
  return <Button Icon={CheckIcon}>OK</Button>;
}

However, this approach throws a typescript error when an icon is passed to the button (<Button Icon={CheckIcon}>OK</Button>):

TS2322: Type 'ForwardRefExoticComponent<Omit<ReactIconProps, "ref"> & RefAttributes<SVGSVGElement>>' is not assignable to type 'ComponentType<ReactIconProps> | undefined'.
  Type 'ForwardRefExoticComponent<Omit<ReactIconProps, "ref"> & RefAttributes<SVGSVGElement>>' is not assignable to type 'FunctionComponent<ReactIconProps>'.
    Types of parameters 'props' and 'props' are incompatible.
      Type 'ReactIconProps' is not assignable to type 'Omit<ReactIconProps, "ref"> & RefAttributes<SVGSVGElement>'.
        Type 'SVGProps<SVGSVGElement>' is not assignable to type 'RefAttributes<SVGSVGElement>'.
          Types of property 'ref' are incompatible.
            Type 'LegacyRef<SVGSVGElement> | undefined' is not assignable to type 'Ref<SVGSVGElement> | undefined'.
              Type 'string' is not assignable to type 'Ref<SVGSVGElement> | undefined'.

The issue is that the icon requires a ref, but we are not passing it one.

Two questions:

  1. How can we specify that the ref passed to icon component is optional?
  2. Is the use of ref in the Icon component a good practice? I have seen lots of advice that refs should not be overused, but see that may component libraries use it extensively.

Edit

I found the answer to #2 in an issue in the heroicons repository - plenty of use cases!


Solution

  • I finally figured out the answer. Instead of typing the Icon prop as ComponentType<ReactIconProps>:

    export interface ButtonProps extends ReactButtonProps {
      Icon?: ComponentType<ReactIconProps>;
    }
    

    I changed it to:

    type ReactIconProps = SVGProps<SVGSVGElement>;
    type ReactIconComponent = React.ForwardRefExoticComponent<
      Omit<ReactIconProps, "ref"> & React.RefAttributes<SVGSVGElement>
    >;
    
    export interface ButtonProps extends ReactButtonProps {
      Icon?: ReactIconComponent;
    }
    

    The new type definition ReactIconComponent essentially replaces the ref property in ReactIconProps with RefAttributes<SVGSVGElement>. This types the ref correctly for an SVG element.

    Here's the fixed CodeSandbox example.