kripod / react-polymorphic-types

Zero-runtime polymorphic component definitions for React
MIT License
195 stars 8 forks source link

Reusing `ref`: type `HTMLElement` is not assignable to type `ElementRef<T>` #4

Closed rafgraph closed 3 years ago

rafgraph commented 3 years ago

Hi, great library, thanks for creating it! I'm using it in React Interactive

When reusing a forwarded ref I get this type error:

Type 'HTMLElement' is not assignable to type 'ElementRef<T>'.

As a workaround I replaced ref: React.ForwardedRef<React.ElementRef<T>> with ref: React.ForwardedRef<HTMLElement>.

I could be typing callbackRef and localRef wrong. I tried replacing HTMLElement with ElementRef<T>, but that doesn't work with subtyping and leads to cascading errors wherever I use localRef, for example when passing localRef.current to someSubtypeFunction I get error Type 'ElementRef<T>' is not assignable to type '{ tagName?: string | undefined; }'.

For a live reproduction see: https://codesandbox.io/s/polymorphic-reuse-ref-pjx00

// works when the argument type is HTMLElement, but not when it is ElementRef<T>
const someSubtypeFunction = (arg: { tagName?: string } | null) => {};

export const Heading: PolymorphicForwardRefExoticComponent<
  HeadingOwnProps,
  typeof HeadingDefaultElement
> = React.forwardRef(function Heading<
  T extends React.ElementType = typeof HeadingDefaultElement
>(
  {
    as,
    color,
    style,
    ...restProps
  }: PolymorphicPropsWithoutRef<HeadingOwnProps, T>,
  ref: React.ForwardedRef<React.ElementRef<T>> // this errors
  // ref: React.ForwardedRef<HTMLElement> // this does not error
) {
  const Element: React.ElementType = as || HeadingDefaultElement;

  // support passed in ref prop as object or callback, and track ref locally
  const localRef = React.useRef<HTMLElement | null>(null);
  const callbackRef = React.useCallback(
    (node: HTMLElement | null) => {
      localRef.current = node;
      if (typeof ref === 'function') {
        ref(node);
      } else if (ref) {
        ref.current = node;
      }
    },
    [ref],
  );

  someSubtypeFunction(localRef.current);

  return <Element ref={callbackRef} style={{ color, ...style }} {...restProps} />;
});
kripod commented 3 years ago

Hello,

I’ve just checked out your reproduction code and noticed that you wanted to type localRef as an HTMLElement. Please type it as React.ElementRef<T> instead.

Also, I recommend using react-merge-refs which offers a clean for this purpose:

import mergeRefs from "react-merge-refs";
import * as React from "react";
import type {
  PolymorphicForwardRefExoticComponent,
  PolymorphicPropsWithoutRef,
  PolymorphicPropsWithRef,
} from "react-polymorphic-types";
import { HeadingDefaultElement, HeadingOwnProps } from "./Heading";

export type HeadingProps<
  T extends React.ElementType = typeof HeadingDefaultElement
> = PolymorphicPropsWithRef<HeadingOwnProps, T>;

export const Heading: PolymorphicForwardRefExoticComponent<
  HeadingOwnProps,
  typeof HeadingDefaultElement
> = React.forwardRef(function Heading<
  T extends React.ElementType = typeof HeadingDefaultElement
>(
  {
    as,
    color,
    style,
    ...restProps
  }: PolymorphicPropsWithoutRef<HeadingOwnProps, T>,
  ref: React.ForwardedRef<React.ElementRef<T>>
) {
  const Element: React.ElementType = as || HeadingDefaultElement;
  const localRef = React.useRef<React.ElementRef<T>>(null);
  return (
    <Element
      ref={mergeRefs([localRef, ref])}
      style={{ color, ...style }}
      {...restProps}
    />
  );
});

You may cast localRef as React.RefObject<Element> or localRef as React.RefObject<HTMLElement> when desired, but the latter isn’t guaranteed to work e.g. when using as="svg".

kripod commented 3 years ago

I’ve just simplified the docs about typing forwarded refs. You can avoid using unsafe as casts by applying the change in https://github.com/kripod/react-polymorphic-types/commit/086bcc99751722d58be92241106b8f24a203c409.

rafgraph commented 3 years ago

Changing ref: React.ForwardedRef<React.ElementRef<T>> to ref: React.ForwardedRef<Element> as per https://github.com/kripod/react-polymorphic-types/commit/086bcc99751722d58be92241106b8f24a203c409 works great, thanks!

Also, good to know about Element vs HTMLElement when using with as="svg", thanks for the info.