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

2

1

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!

Laveta answered 25/12, 2023 at 19:54 Comment(2)
Why do you need to use a ref in the first place? You're not doing anything with it in Button that I can seeAmmonium
I don't need the ref in the button. But the icon library has been written to be used by many other components, hence the author of that component has added a ref there. That's why I asked the 2nd question: why do many libraries do this? For example: Button in shad/uiLaveta
L
1

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.

Laveta answered 26/12, 2023 at 22:10 Comment(0)
U
0

You can make any field optional in your interface declaration by partial extensions of the original type or whatever part you need. (This can also be done via omit, &, as, etc)

For example:

interface ExampleProps extends Partial<ReactIconProps>{ customItem:string; }

ExampleProps now contains all fields in ReactIconProps, but as optional items. With customItem being the only required field.

Uranology answered 25/12, 2023 at 23:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.