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}
/>
The output:
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
}
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:
<T>
Instead of using any
we can pass a generic type to our component:
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'},
]
<TabBar<MySocial>
selectedItem={selectedSocial}
items={socials}
onTabClick={selectHandler}
/>
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 ... */
}
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>
</>
)
}
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>
)
};