How to use generics in React components
Building highly reusable UI components often forces a compromise between flexibility and type safety.
With over 25 years of experience in software development and as the creator of CoreUI, I have architected hundreds of production-ready components that leverage TypeScript generics to solve this exact problem.
The most efficient and modern solution is to use generic type parameters in your component definitions, allowing the component to adapt to any data structure while maintaining strict type checking.
This approach ensures that your components are both robust and developer-friendly, providing full intellisense for whatever data is passed to them.
Use generic type parameters <T> in your component definition to allow consumers to pass custom data structures while maintaining full TypeScript intellisense.
interface ListProps<T> {
items: T[]
renderItem: (item: T) => React.ReactNode
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <div>{items.map(renderItem)}</div>
}
Creating a Generic List Component
A generic list is the most fundamental use case for generics in React. It allows you to pass an array of any object type and define how each item should be rendered.
import React from 'react'
interface ListProps<T> {
items: T[]
renderItem: (item: T) => React.ReactNode
keyExtractor: (item: T) => string | number
}
export function GenericList<T>({
items,
renderItem,
keyExtractor
}: ListProps<T>) {
return (
<ul className='list-group'>
{items.map((item) => (
<li key={keyExtractor(item)} className='list-group-item'>
{renderItem(item)}
</li>
))}
</ul>
)
}
// Usage Example
interface User {
id: number
name: string
}
const UserList = () => {
const users: User[] = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
]
return (
<GenericList
items={users}
keyExtractor={(user) => user.id}
renderItem={(user) => <span>{user.name}</span>}
/>
)
}
This implementation allows the GenericList to accept an array of User objects, Product objects, or even simple strings. TypeScript automatically infers the type T from the items prop, so the renderItem function knows exactly what properties are available on the item argument. This is a pattern we use extensively in our React Dashboard Template to handle dynamic data streams.
Generic Select Components with CoreUI
When building form elements like a MultiSelect, generics are essential for handling complex option objects while keeping the onChange event typed.
import React from 'react'
import { CFormSelect } from '@coreui/react'
interface Option {
label: string
value: string | number
}
interface GenericSelectProps<T extends Option> {
options: T[]
value: T['value']
onChange: (value: T['value']) => void
label: string
}
export function GenericSelect<T extends Option>({
options,
value,
onChange,
label
}: GenericSelectProps<T>) {
return (
<div className='mb-3'>
<label className='form-label'>{label}</label>
<CFormSelect
value={String(value)}
onChange={(e) => {
const raw = e.target.value
const parsed = options.find((o) => String(o.value) === raw)
if (parsed) onChange(parsed.value)
}}
>
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</CFormSelect>
</div>
)
}
// Usage with extended types
interface CategoryOption extends Option {
color: string
}
const categories: CategoryOption[] = [
{ label: 'Development', value: 'dev', color: 'blue' },
{ label: 'Design', value: 'des', color: 'pink' }
]
const MyForm = () => {
const [selected, setSelected] = React.useState('dev')
return (
<GenericSelect
label='Pick a Category'
options={categories}
value={selected}
onChange={(val) => setSelected(String(val))}
/>
)
}
In this section, we use T extends Option to enforce a constraint. This ensures that whatever type is passed to the component, it must at least contain a label and a value. The onChange handler looks up the original option to preserve the correct value type instead of always returning a string from the DOM. For more on typing React events, see how to type events in React with TypeScript.
Generic Table with Dynamic Columns
A Smart Table often requires generics to map data properties to table columns safely.
import React from 'react'
import { CTable, CTableHead, CTableRow, CTableHeaderCell, CTableBody, CTableDataCell } from '@coreui/react'
interface Column<T> {
header: string
key: keyof T
render?: (value: T[keyof T], item: T) => React.ReactNode
}
interface GenericTableProps<T> {
data: T[]
columns: Column<T>[]
}
export function GenericTable<T>({ data, columns }: GenericTableProps<T>) {
return (
<CTable hover responsive>
<CTableHead>
<CTableRow>
{columns.map((col, index) => (
<CTableHeaderCell key={index}>{col.header}</CTableHeaderCell>
))}
</CTableRow>
</CTableHead>
<CTableBody>
{data.map((item, rowIndex) => (
<CTableRow key={rowIndex}>
{columns.map((col, colIndex) => (
<CTableDataCell key={colIndex}>
{col.render
? col.render(item[col.key], item)
: String(item[col.key])
}
</CTableDataCell>
))}
</CTableRow>
))}
</CTableBody>
</CTable>
)
}
// Data example
const tableData = [
{ id: '1', status: 'Active', amount: 500 },
{ id: '2', status: 'Pending', amount: 1200 }
]
const MyDashboard = () => (
<GenericTable
data={tableData}
columns={[
{ header: 'ID', key: 'id' },
{ header: 'Total', key: 'amount', render: (val) => `$${val}` }
]}
/>
)
The use of keyof T ensures that the columns configuration only allows keys that actually exist on the data objects. This prevents runtime errors and makes the component much more predictable. For a similar approach to typing component props with constraints, see how to type props in React with TypeScript.
Constraining Generics with Interfaces
Sometimes you need to ensure that the generic type passed to a component has specific properties, such as an id or a slug.
import React from 'react'
interface Identifiable {
id: string | number
}
interface CardProps<T extends Identifiable> {
item: T
titleKey: keyof T
contentKey: keyof T
}
export function GenericCard<T extends Identifiable>({
item,
titleKey,
contentKey
}: CardProps<T>) {
return (
<div className='card mb-3' id={`item-${item.id}`}>
<div className='card-body'>
<h5 className='card-title'>{String(item[titleKey])}</h5>
<p className='card-text'>{String(item[contentKey])}</p>
</div>
</div>
)
}
// Component Usage
interface Product extends Identifiable {
title: string
description: string
price: number
}
const product: Product = {
id: 'p1',
title: 'CoreUI Pro',
description: 'Enterprise UI Components',
price: 99
}
const ProductView = () => (
<GenericCard
item={product}
titleKey='title'
contentKey='description'
/>
)
By extending Identifiable, we guarantee that item.id will always be accessible. This is a common pattern for components that need to interact with DOM IDs or API endpoints. It is significantly more robust than relying on loosely typed props.
Handling Generics in Arrow Functions
The syntax for generics in arrow functions can be tricky because TSX parsers might mistake the generic bracket for an HTML tag.
import React from 'react'
interface WrapperProps<T> {
value: T
children: (value: T) => React.ReactNode
}
// Note the comma after T to help the TSX parser
export const GenericWrapper = <T,>({ value, children }: WrapperProps<T>) => {
return (
<div className='generic-container'>
{children(value)}
</div>
)
}
const App = () => (
<GenericWrapper value={{ theme: 'dark', version: 5 }}>
{(config) => (
<div>
Running CoreUI version {config.version} in {config.theme} mode
</div>
)}
</GenericWrapper>
)
The <T,> syntax is a standard workaround in .tsx files to disambiguate generics from JSX tags. Without that comma, the compiler might throw an error thinking you’ve started an unclosed T element. This ensures your functional components remain clean and modern.
Generic ForwardRefs
Using forwardRef with generics requires a slightly different approach because forwardRef doesn’t natively support generic parameters easily.
import React, { forwardRef, ForwardedRef } from 'react'
interface InputProps<T> {
value: T
label: string
onValueChange: (val: T) => void
}
function GenericInputInner<T>(
props: InputProps<T>,
ref: ForwardedRef<HTMLInputElement>
) {
return (
<div className='input-group'>
<label>{props.label}</label>
<input
ref={ref}
value={String(props.value)}
onChange={(e) => props.onValueChange(e.target.value as unknown as T)}
/>
</div>
)
}
// Cast to allow generics
export const GenericInput = forwardRef(GenericInputInner) as <T>(
props: InputProps<T> & { ref?: ForwardedRef<HTMLInputElement> }
) => ReturnType<typeof GenericInputInner>
This pattern allows you to maintain both the benefits of forwardRef (accessing the underlying DOM node) and generics. While it looks complex, it is the standard way to handle advanced component patterns in professional libraries. For a deeper dive into forwardRef typing patterns, see how to type forwardRef in React with TypeScript.
Best Practice Note:
When using generics, always aim for the most restrictive constraint possible. Instead of using T extends any, identify the minimum set of properties your component needs to function and define an interface for them. This is the same approach we use in CoreUI components to ensure reliability across thousands of different implementations. Over-using generics can lead to code that is hard to read, so apply them only when a component truly needs to handle multiple, unknown data structures. For simpler cases, standard interfaces are often sufficient.



