npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@gomah/react-polymorphed

v2.2.3

Published

Build fast type-safe Polymorphic components with ease.

Downloads

57

Readme

react-polymorphed

A set of types to help easily create fast polymorphic components. This package heavily relied on react-polymorphic-types when it was being made.

Basic Usage

Let's start with creating a polymorphic button component.

import { PolymorphicComponent } from "react-polymorphed";

type Props = {
  size?: "small" | "large";
};

// pass it the default type and your own props
const Button: PolymorphicComponent<"button", Props> = ({
  as: As = "button",
  size,
  ...props
}) => {
  return <As {...props} />;
};

We can then use this polymorphic component like so:

  <Button type="submit" size="small"> I am a button!</Button>
  <Button as={"a"} href="" size="large"> I became an achor!</Button>
  <Button href="">I cannot have an href!</Button> //error

Supporting forwardRef()

The easiest way to create ref-forwarded polymorphic components is to cast the forwardRef function to a PolyRefFunction:

import { forwardRef } from "react";
import { PolyRefFunction } from "react-polymorphed";

const polyRef = forwardRef as PolyRefFunction;

type Props = {
  size?: "small" | "large";
};

const Button = polyRef<"button", Props>(
  ({ as: As = "button", size, ...props }, ref) => {
    return <As ref={ref} {...props} />;
  }
);

This should now expose the ref property and will correctly change it's type based on the as prop. If the component given to the as prop does not support refs then it will not show.

const Example = () => {
  const buttonRef = useRef<HTMLButtonElement>(null);
  return (
    <>
      <Button ref={buttonRef} />
      // error! type of ref don't match
      <Button as="div" ref={buttonRef} />
      // error! property ref doesn't exist
      <Button as={() => null} ref={buttonRef} />
    </>
  );
};

Typing memo() and lazy()

Unlike React.forwardRef(), memo and lazy doesn't need any special functions to make work, we can simply assign it's type correctly like so:

import React from "react";
import {
  PolymorphicComponent,
  PolyMemoComponent,
  PolyLazyComponent,
} from "react-polymorphed";

type Props = {
  size?: "small" | "large";
};

const Button: PolymorphicComponent<"button", Props> = ({
  as: As = "button",
  size,
  ...props
}) => {
  return <As {...props} />;
};

const MemoButton: PolyMemoComponent<"button", Props> = React.memo(Button);

// in another file:
const LazyButton: PolyLazyComponent<"button", Props> = React.lazy(
  async () => import("./Button")
);

memo() and lazy() with polyRef()

Note that if the polymorphic component forwards refs, you need to instead use either the PolyForwardMemoComponent or PolyForwardLazyComponent to correctly preserve the ref property.

import React from "react";
import { PolyRefFunction, PolyForwardMemoComponent } from "react-polymorphed";

const polyRef = React.forwardRef as PolyRefFunction;

type Props = {
  size?: "small" | "large";
};

const RefButton = polyRef<"button", Props>(
  ({ as: As = "button", size, ...props }, ref) => {
    return <As ref={ref} {...props} />;
  }
);

// use the correct type!
const MemoRefButton: PolyForwardMemoComponent<"button", Props> =
  React.memo(RefButton);

Adding Constraints

Say you wanted your button to only be "button" | "a", you can pass a third type to the PolymorphicComponent with OnlyAs<T>:

import React from "react";
import { PolymorphicComponent, OnlyAs } from "react-polymorphed";

const Button: PolymorphicComponent<"button", {}, OnlyAs<"button" | "a">> = ({
  as: As = "button",
  ...props
}) => {
  return <As {...props} />;
};

<Button />;
<Button as="a" />;
<Button as="div" />; // error!

⚠️ Hold up! It has occured to me that constraints may not be a good feature to use and could even do more harm than good so before you use constraints it is important that you read the FAQ below on why you might not want them.

FAQs

Using something like ElementType<{ required: string }> will not work on components which do not have any props:

type A = () => null;
type B = ElementType<{ required: string }>;
type DoesExtend = A extends B ? true : false; // true!

There's really no solution to fix this at the moment since this is a problem with typescript itself, and to no fault from typescript because the type of A CAN technically be called with the props of B because A won't use those props anyway, and since ReturnType<A> extends ReturnType<B> there is no reason for A to not extend B.

So unless you really need constraints and you and your team fully expect this behavior and other weird behaviors that comes from it, maybe you shouldn't use this at all. however, constraints that are purely just elements (e.g "button" | "a") will probably work just fine.

Just "button" | "a" could do the trick but we then have a problem of our props being "known" and typescript will complain that props don't match. see issue #3

const Button: PolymorphicComponent<"button", {}, "button" | "a"> = ({
  as: As = "button",
  ...props
}) => {
  // error when props are spread
  return <As {...props} />;
};

OnlyAs<T> solves this by adding another type to the contraint, a type that makes our props unknown:

// ComponentProps<"button"> | ComponentProps<"a">
type A = ComponentPropsWithoutRef<"button" | "a">;

// unknown
type B = ComponentPropsWithoutRef<OnlyAs<"button" | "a">>;

// what OnlyAs is doing:
type C = ComponentPropsWithoutRef<
  "button" | "a" | (() => React.ReactElement<never>)
>;

If you're having trouble with props being required on the <As /> component, you can widen the type by casting it to React.ElementType. see issue #5

polyRef<"button", {}, OnlyAs<"button" | "a" | typeof Link>>(({ as: As = "button", ...props }, ref) => {
  const Elem = As as React.ElementType;
  return <Elem ref={ref} />
});

It might help if you wrap your string around an {} block, it could show the full list of suggestions, doesn't fully work though.