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:
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'},
]
2) We can pass this type to the component as generic:
<TabBar<MySocial>
selectedItem={selectedSocial}
items={socials}
onTabClick={selectHandler}
/>
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 ...
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>
</>
)
}
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>
)
};
Result:
You can also be interested to read this article:
How to create React UIKIT components in TypeScript that extends native HTML Elements
Discussion (6)
Nice explanation for generics in TSX, but just in case, problems like the one in the post (tabs) should be solved in a simpler way, by splitting into a
Tab
component and aTabBar
component instead of making theTabBar
map automatically trough an array:The good thing about this approach is that the deb still can customize the
Tabs
, or add a static Tab at some point, or any other element. Way more flexible with pretty much the same amount of code.Cheers!
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).
not simple and stupid enough
do you mean the example should be simpler?
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
ah ok.. Sure. It's just an example of use case.
I agree and I don't know use it a lot too : )