Polymorphic Components in React with TypeScript

React Polymorphic components are a powerful pattern in React development, enabling us to create flexible and reusable components. These components can adapt to different HTML elements or custom components while retaining their own functionalities and props. This flexibility is achieved using the as prop, a concept integral to polymorphic components.

1. Basic Polymorphic Component

The "as" prop in polymorphic components allows the component to change the type of the rendered element dynamically. This means you can have a single component that can act as a div, span, header, or any custom component, depending on your needs. This approach is incredibly useful for building a design system or consistent UI with a shared set of styling and behavior rules, without repeating code.

Let's start by creating a simple polymorphic component named Head. This component will accept a prop as, which determines the type of element or component it renders. Additionally, it will take children, the standard prop for passing elements to be rendered inside the component.

import { type ReactNode, type ElementType } from "react"

type HeadProps = {
    as: ElementType
    children: ReactNode
}

const Head = ({ as: Component, children }: HeadProps) => {
    return <Component>{children}</Component>
}

export default Head        

  1. Importing Types: We import ReactNode and ElementType from React. ReactNode is a type that covers anything that can be rendered in React (like strings, elements, or an array of these), and ElementType represents a valid React component.
  2. Defining HeadProps: This type defines the props for our Head component. It includes: as: An ElementType, determining the type of component to render. children: A ReactNode, representing the child elements of our component.
  3. Creating the Head Component: The Head component is a functional component that takes HeadProps as its props. It uses JSX to render the component passed as the as prop, enclosing any children passed to it.

Usage example:

import './App.css'
import Head from './Head'

function App() {

  return (
    <>
      <Head as='h1'>H1 header</Head>
      <Head as='h2'>H2 header</Head>
    </>
  )
}

export default App
        

This is exactly what we see, two headings, h1 and h2:

2. Advanced Polymorphic Component

A common challenge arises when we want to add more functionality, like event handlers. Let's delve into how we can enhance our polymorphic components to handle such cases. Suppose we want to add an onClick event listener to our component. The current implementation of HeadProps only accepts as and children props, limiting our component's flexibility. To address this, we need to accept all props of the concrete component specified by the as prop. In order To incorporate additional props, we'll use the utility type ComponentPropsWithoutRef. This type can be used to grab all the native attributes of an HTML element as the props type of our component. The "problem" now is that we don't know the type of the component that will be passed in, and the solution for this is to create a new generic type of out own.

type HeadProps<T> = {
    as: T
    children: ReactNode
} & ComponentPropsWithoutRef<T>        

Now, ComponentPropsWithoutRef will extract the props of the incoming T component. The type of "as" is simply T because it represents the element itself. However, technically, T could currently be anything, such as a number or a string, which wouldn't be valid as an element. To address this issue, TypeScript allows us to extend the base type that T is based upon.

type HeadProps<T extends ElementType> = {
    as: T
    children: ReactNode
} & ComponentPropsWithoutRef<T>        

Now, as can be seen in the screen shot, Typescript complains that HeadProps is a generic type and requires 1 type argument(s)

To fix this, we need to adjust the type of the Head component accordingly. This means it also must be a generic type, which we'll mark as C. Since C could potentially be anything, again, we need to constrain it as well. We'll do this by ensuring that it extends only ElementType to guarantee that it can only be a component and not a string, for example.

At this point Typescript is still not happy and complains about "Type '{ children: ReactNode; }' is not assignable to type 'LibraryManagedAttributes<C, any>'.ts(2322)"

A common pattern to resolve this issue involves defining the "as" prop as a Component or 'div' as the default component (see "comment"), and marking the "as" prop as optional. This way, TypeScript understands that Component will always be a valid element that can be used in JSX and serve as a wrapper for the children prop.

import { type ReactNode, type ElementType, type ComponentPropsWithoutRef } from "react"

type HeadProps<T extends ElementType> = {
    as?: T // The "comment"
    children: ReactNode
} & ComponentPropsWithoutRef<T>

const Head = <C extends ElementType>({ as, children }: HeadProps<C>) => {
    const Component = as || 'div' // The "comment"
    return <Component>{children}</Component>
}
 
export default Head        

By this time we eliminated all Typescript errors and we can implement the final stage of forwarding the rest of the Props. We need to accept "...props" in our component, allowing any extra props to be passed and handled correctly. This makes our Head component versatile and capable of handling various props, including event listeners.

import { type ReactNode, type ElementType, type ComponentPropsWithoutRef } from "react"

type HeadProps<T extends ElementType> = {
    as?: T
    children: ReactNode
} & ComponentPropsWithoutRef<T>

const Head = <C extends ElementType>({ as, children, ...props }: HeadProps<C>) => {
    const Component = as || 'div'
    return <Component {...props}>{children}</Component>
}

export default Head        

For example, now we can add click event handling to our Head component to enhance its flexability.

How to use:

function App() {
  const handleClick = () => {
    console.log('clicked')
  }

  return (
      <Head as='h1' onClick={handleClick}>Clickable H1 header</Head>
  )
}

export default App        

Conclusion

In this article, we've enhanced our polymorphic component using TypeScript to be more flexible and capable of handling various props, including event listeners. This approach significantly boosts the reusability and adaptability of our components, making them an invaluable tool in a React developer's toolkit.


Ciao

pouria kalantari

Senior frontend developer | React, Typescript | Open to Relocation

9 个月

Nice but I want to refer to this https://blog.logrocket.com/build-strongly-typed-polymorphic-components-react-typescript/ article .may be take a short glance of it can reveal more secrets from this article

回复
Riad Hallouch

Frontend developer React | Next | Node | Typescript | Tailwindcss

11 个月

I actually feel like a senior dev after understanding this article ?? LOL, Thank you man!

要查看或添加评论,请登录

Alex Tal的更多文章

  • Measure Performance Like A Pro

    Measure Performance Like A Pro

    When developing applications, understanding and optimizing performance is crucial. Often, you may have come across a…

    1 条评论
  • Diving into Web Workers

    Diving into Web Workers

    At its core, JavaScript is a single-threaded language, which means it executes one operation at a time in a single…

  • Decoding Web Components: Insights into Shadow DOM and Its Impact

    Decoding Web Components: Insights into Shadow DOM and Its Impact

    As part of the frontend infrastructure team at Tipalti, our journey in building a robust design system has been greatly…

    1 条评论
  • Understanding JS Promise Methods and the Importance of Event Loop

    Understanding JS Promise Methods and the Importance of Event Loop

    Promises are a fundamental part of asynchronous programming in JavaScript, and their associated methods – Promise.all…

    2 条评论
  • Angular 16 Introduces Signals

    Angular 16 Introduces Signals

    Angular 16, the latest major release of the Angular web development framework, introduces a new feature called signals.…

社区洞察

其他会员也浏览了