polymorphic-react-component icon indicating copy to clipboard operation
polymorphic-react-component copied to clipboard

How do I wrap my Polymorphic Component?

Open mrbinky3000 opened this issue 1 year ago • 2 comments

Thank you for your excellent tutorial. I'm still new to Typescript.

I'm not sure how to write a component that wraps my Polymorphic created using your tutorial. Lets call the polymorphic component "Atom" for the sake of this question.

The wrapper component is called "Molecule" and I want "Molecule" to accept have all the same props as "Atom" plus a few more.

Should my wrapper component also use your Polymorphic types to take advantage of the strict typing? That's what I'm doing right now. There is a small problem, though. I can't pass the "as" prop from Molecule to Atom.

As an example, I've copied your example code for the Text component and also added a Wrapper component that wraps Text and provides an additional style. I can't use Wrapper to pass the "as" prop to the Text component.

import React from 'react';

type AsProp<C extends React.ElementType> = { as?: C };

type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);

// This is the first reusable type utility we built
type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = object,
> = React.PropsWithChildren<Props & AsProp<C>> &
  Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;

// This is a new type utility with ref!
type PolymorphicComponentPropWithRef<
  C extends React.ElementType,
  Props = object,
> = PolymorphicComponentProp<C, Props> & { ref?: PolymorphicRef<C> };

// This is the type for the "ref" only
type PolymorphicRef<C extends React.ElementType> =
  React.ComponentPropsWithRef<C>['ref'];

// -----------------------------------------------------------------------------
// Text Component
// -----------------------------------------------------------------------------

type Rainbow =
  | 'red'
  | 'orange'
  | 'yellow'
  | 'green'
  | 'blue'
  | 'indigo'
  | 'violet';

/**
 * This is the updated component props using PolymorphicComponentPropWithRef
 **/
type TextProps<C extends React.ElementType> = PolymorphicComponentPropWithRef<
  C,
  { color?: Rainbow | 'black' }
>;

/**
 * This is the type used in the type annotation for the component
 **/
type TextComponent = <C extends React.ElementType = 'span'>(
  props: TextProps<C>
) => React.ReactNode | null;

export const Text: TextComponent = React.forwardRef(function Text<
  C extends React.ElementType = 'span',
>({ as, color, children }: TextProps<C>, ref?: PolymorphicRef<C>) {
  const Component = as ?? 'span';
  const style = color ? { style: { color } } : {};
  return (
    <Component {...style} ref={ref}>
      {' '}
      {children}{' '}
    </Component>
  );
});

// -----------------------------------------------------------------------------
// Wrapper Component
// -----------------------------------------------------------------------------

/**
 * This is the updated component props using PolymorphicComponentPropWithRef
 **/
type WrapperProps<C extends React.ElementType> =
  PolymorphicComponentPropWithRef<
    C,
    {
      color?: Rainbow | 'black';
      size?: 'big' | 'small';
    }
  >;

/**
 * This is the type used in the type annotation for the component
 **/
type WrapperComponent = <C extends React.ElementType = 'span'>(
  props: WrapperProps<C>
) => React.ReactNode | null;

export const Wrapper: WrapperComponent = React.forwardRef(function Text<
  C extends React.ElementType = 'span',
>(
  { as, color, children, size = 'small' }: WrapperProps<C>,
  ref?: PolymorphicRef<C>
) {
  const style = {
    style: {
      color,
      fontSize: size === 'small' ? '10px' : '40px',
    },
  };
  return (
    <Text 
      as={as}  // 👈 This causes a typeerror in Text.  If I set it to as='div' it works. But I want wrapper to be able to specify a tag.  How do we make as a string?
      {...style} 
      ref={ref}
    >
      {children}
    </Text>
  );
});

mrbinky3000 avatar Dec 12 '23 04:12 mrbinky3000

Well, I think I figured it out? Can you tell me if you agree with this approach? The code is the same as above, I just changed things after the comment labeled "Wrapper Component"

// -----------------------------------------------------------------------------
// Wrapper Component
// -----------------------------------------------------------------------------

type WrapperProps<C extends React.ElementType = 'span'> = {
  size?: 'big' | 'small';
  color?: string;
  children: React.ReactNode;
  as: C
} & PolymorphicRef<C>

type WrapperComponent = <C extends React.ElementType = 'span'>(
  props: WrapperProps<C>
) => React.ReactNode | null;

export const Wrapper: WrapperComponent = React.forwardRef(function Text<
  C extends React.ElementType = 'span',
>(
  { as, color, children, size = 'small' }: WrapperProps<C>,
  ref?: PolymorphicRef<C>
) {
  const style = {
    style: {
      color,
      fontSize: size === 'small' ? '10px' : '40px',
    },
  };
  return (
    <Text
      as={as} // 👈 This seems satisfied now
      {...style}
      ref={ref}
    >
      {children}
    </Text>
  );
});

mrbinky3000 avatar Dec 12 '23 05:12 mrbinky3000

Nope. That doesn't work. Wrapper will accept <Wrapper as="button" href="/test">Hello</Wrapper> and as we know, buttons don't have hrefs.

mrbinky3000 avatar Dec 12 '23 15:12 mrbinky3000