DEV Community

Cover image for Create a React / TypeScript Generic Component
Fabio Biondi
Fabio Biondi

Posted on • Updated on

Create a React / TypeScript Generic Component

Often we need to create generic components in React / TypeScript that need to accept any kind of type.

Since we want to create reusable components and, at the same time, they should be type-safed too, we cannot define its own props as any type, and unknown is not often a valid solution.

Now let's imagine if we have to create a TabBar component in React/TypeScript that accepts an items property of any type of array(string[], User[], Whatever[]):

<TabBar
  items={anyTypeOfArray}
  onTabClick={selectHandler}
/>
Enter fullscreen mode Exit fullscreen mode

The output:

Demo

If the TabBar items property should accept any kind of type we may think to use any[]. Right? Ehm... no 😅
We completely lose type checking!

interface TabBarProps<T> {
  items: any[];
  selectedItem: any;
  onTabClick: (item: any, selectedIndex: number) => void
}
Enter fullscreen mode Exit fullscreen mode

In fact, by using any, the TypeScript compiler and your IDE/editor are not able to know which type of parameters your onTabClick will come back or what type of data selectedItem should accepts:

Ide problem

Solution

Instead of using any we can pass a generic type to our component:

1) First, we create a custom type (in this example MySocial but it could be anything):

interface MySocial {
  id: number;
  name: string;
  link: string;
}

const socials: MySocial[] = [
  { id: 11, name: 'WebSite', link: 'https://www.fabiobiondi.dev'},
  { id: 12, name: 'Youtube', link: 'https://www.youtube.com/c/FabioBiondi'},
  { id: 13, name: 'Twitch', link: 'https://www.twitch.tv/fabio_biondi'},
]
Enter fullscreen mode Exit fullscreen mode

2) We can pass this type to the component as generic:

<TabBar<MySocial>
  selectedItem={selectedSocial}
  items={socials}
  onTabClick={selectHandler}
/>
Enter fullscreen mode Exit fullscreen mode

3) Our TabBar component should now use generics instead of any.
We can also decide this type must includes id and name in its definition:

interface TabBarProps<T> {
  items: T[];
  selectedItem: T;
  onTabClick: (item: T, selectedIndex: number) => void
}

export function TabBar<T extends { id: number, name: string}>(props: TabBarProps<T>) {

  // ... your component code here ...
Enter fullscreen mode Exit fullscreen mode

Final Source Code

Here the complete source code of TabBar (it uses Tailwind for CSS but it doesn't matter) :

// TabBar.tsx
interface TabBarProps<T> {
  items: T[];
  selectedItem: T;
  onTabClick: (item: T, selectedIndex: number) => void
}

export function TabBar<T extends { id: number, name: string}>(props: TabBarProps<T>) {
  const { items, selectedItem, onTabClick} = props;
  return (
    <>
      <div className="flex gap-x-3">
        {
          items.map((item, index) => {
            const activeCls = item.id === selectedItem.id ? 'bg-slate-500 text-white' : ' bg-slate-200';
            return <div
                key={item.id}
                className={'py-2 px-4 rounded ' + activeCls}
                onClick={() => onTabClick(item, index)}
              >
                {item.name}
              </div>
            }
          )
        }
      </div>
    </>
  )
}

Enter fullscreen mode Exit fullscreen mode

Usage

Following an example of usage:

// App.tsx
import { useState } from 'react';
import { TabBar } from '../../../shared/components/TabBar';

interface MySocial {
  id: number;
  name: string;
  link: string;
}

const socials: MySocial[] = [
  { id: 11, name: 'WebSite', link: 'fabiobiondi.dev'},
  { id: 12, name: 'Youtube', link: 'YT'},
  { id: 13, name: 'Twitch', link: 'twitch'},
]

export const App = () => {
  const [selectedSocial, setSelectedSocial] = useState<MySocial>(socials[0])

  function selectHandler(item: MySocial, selectedIndex: number) {
    setSelectedSocial(item)
  }

  return (
    <div>
      <h1>Tabbar Demo</h1>
        <TabBar<MySocial>
          selectedItem={selectedSocial}
          items={socials}
          onTabClick={selectHandler}
        />

      <div className="border border-slate-200 border-solid rounded my-3 p-5">
        <a href={selectedSocial.link}>Visit {selectedSocial.name}</a>
      </div>
    </div>
  )
};

Enter fullscreen mode Exit fullscreen mode

Result:

Final Demo Animated


You can also be interested to read this article:
How to create React UIKIT components in TypeScript that extends native HTML Elements

Top comments (8)

Collapse
 
fabiobiondi profile image
Fabio Biondi

yes, there is often an alternative way to to things.
That's just a simple example I did for a course a couple of days ago and I have shared it :)

offtopic: I'm removing all React.FC and VFC (deprecated) from my components and I'm using again to fn(props: MyType) and for children fn(props: PropsWithChildren).

Collapse
 
sivaneshs profile image
Sivanesh Shanmugam

It helped me a lot today. Thanks for the great blog!

Collapse
 
uzoamaka126 profile image
Uzoamaka Anyanwu

You simplified something I was trying to understand for a while and I can't thank you enough. Thank you for putting this out!

Collapse
 
mohajerimasoud profile image
Masoud Mohajeri

not simple and stupid enough

Collapse
 
fabiobiondi profile image
Fabio Biondi

do you mean the example should be simpler?

Collapse
 
mohajerimasoud profile image
Masoud Mohajeri

No , the whole Generic Component idea is not simple and stupid enough.
there are some good use cases and I have used it a couple of times but usually is not the best choice

Thread Thread
 
fabiobiondi profile image
Fabio Biondi

ah ok.. Sure. It's just an example of use case.
I agree and I don't know use it a lot too : )

Thread Thread
 
thiago-coderr profile image
Tiago_908

I cannot setup my react project with vite. Can you please help me