a small ninja

How to create React UIKIT components

in TypeScript that extends native HTML Elements

How to create React UIKIT components in TypeScript that extends native HTML Elements

2022-09-28
5 min read
Difficulty
react
typescript
a small ninja

TOPICS

Introduction

In the front-end ecosystem, UiKits are collections of utilities and several components, such as:

  • <Card />
  • <TabBar />
  • <Carousel />
  • <Row /> and <Col />
  • <GoogleMap />
  • Each component has usually its own custom layout and could accepts several properties. For example, a <GoogleMap /> component could accept the "coordinates" and a "zoom" value as properties:

    <GoogleMap coords={coords} zoom={zoom} />

    Sometimes we need to create components whose layout is created from scratch. In other cases their layout may be based of native elements. Some examples:

    • <MyButton />: that extends <button> element capabilities
    • <MyImg />: that extends <img> element
    • and so on.

    Let's imagine a button component that must display an icon next to the label that I can use as the code below:

    <MyButton icon="💩"> CLICK ME </MyButton>

    This component should accept the icon and children properties and its definition may looks like the following:

    interface MyButtonProps {
      icon: string;
    }
    
    function MyButton(props: PropsWithChildren<MyButtonProps>) {
      const { icon, children } = props;
      return <button className="btn btn-primary">
        {icon} {children}
      </button>
    }

    So we can use the component in this way:

    <MyButton icon="💩"> DO SOMETHING</icon>
    <MyButton icon="😱"> CLICK ME</icon>

    Button Preview

    Extend with HTML Props

    In order to be more flexible, the <MyButton /> component should also accepts all the native button properties. For example we may need to listen the onClick event or set the disabled attribute:

    <MyButton icon="💩" onClick={() => {}} disabled />

    So we can simply add them to the Component's Property Type and apply them as attributes of the <button> element:

    // 1. Add the properties 
    interface MyButtonProps {
      icon: string;  
      disabled: boolean;   // ➡️ native prop
      onClick: (e: React.MouseEvent) => void; // ➡️ native prop
      // ➡️ other native props
    }
    
    function MyButton(props: PropsWithChildren<MyButtonProps) {
    
      const { icon, children, onClick, disabled } = props;
      // 2. apply all props one by one
      return <button disabled={disabled} onClick={onClick} className="btn btn-primary">
        {icon}
        {children}
      </button>
    }

    What could I do to avoid manually passing all properties of a native button?

    There is a trick!

    We can simply use an intersection type to combine our custom props with all the HTMLButtonElement properties to automatically support all native buttons properties:

    export function MyButton(
      props: PropsWithChildren<MyButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>>
    ) { 
     // ... component here ...
    }

    Now your component supports all buttons properties and we can apply them simply using this trick:

    // apply all props as button attributes
    return <button className="btn btn-primary" {...props}>

    However icon and children are not valid button's properties so we can use destructuring to create a rest property that contain all the properties except them:

    const { icon, children, ...rest } = props;
    
    // Now we apply all props except icons and children
    return <button className="btn btn-primary" {...rest}>
      {icon}
      {children}
    </button>

    And that's all. Here the final source code to create a button that support icon, children and all native button properties:

    import { PropsWithChildren } from 'react';
    
    interface MyButtonProps {
      icon: string;
    }
    export function MyButton(
      props: PropsWithChildren<MyButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>>
    ) {
      const { icon, children, ...rest } = props;
      return <button className="btn btn-primary" {...rest}>
        {icon}
        {children}
      </button>
    }
    

    How to Use

    <MyButton 
      icon="💩" 
      type="submit"
      disabled={...} 
      onClick={...} 
      onMouseOver={...} 
    > CLICK ME </MyButton>

    Code Playground

    Keep updated about latest content
    videos, articles, tips and news