Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

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.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author