Creating Reusable React Components with Generics

Learn how to use generics in React functional components to create reusable and type-safe code. This guide covers step-by-step instructions for building flexible components that handle various data types.

Published on

4 min read

Imagine we're working on a React project and we have two components: one for displaying a list of users and another for displaying a list of products. Without using generics, we might end up with creating 2 components with similar code.

interface User {
  id: number;
  name: string;
}

interface UserListProps {
  users: User[];
}

export function UserList({ users }: UserListProps) {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
interface Product {
  id: number;
  name: string;
  price: number;
}

interface ProductListProps {
  products: Product[];
}

export function ProductList({ products }: ProductListProps) {
  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          {product.name} - ${product.price}
        </li>
      ))}
    </ul>
  );
}

The Problem

Duplicating code: Both UserList and ProductList components have almost identical structures.

A better solution is to create a reusable list component that can handle both users and products! Flexibility is key! This is where generics come in handy.

How to Use Generics in React Components

Now, let's dive into how we can use generics to create flexible and reusable components.

First, we'll define a generic component: a List component that can accept an array of any type:

1. Define the Props Interface:

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}
  • ListProps<T> : an interface that takes a generic type T.
  • items is an array of type T.
  • renderItem is a builder function that takes an item of type T and returns a React node. This function allows us to customize how each item is rendered.

2. Define the Generic Component:

export function List<T>({ items, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}
  • List<T> is a component that takes props of type ListProps<T>.
  • We use the map method to iterate over the items array and render each item using the renderItem function. Note that we will not handle how the item is rendered.
  • We use the index as the key for each list item. For a real-world application, it's better to use a unique identifier if available.

The complete code:

import React from 'react';

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

export function List<T>({ items, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

Using the Generic Component

Now that we've defined our generic List component, let's use it with different data types.

Example 1: List of Users

import React from 'react';
import List from './List';

interface User {
  id: number;
  name: string;
}

const users: User[] = [
  { id: 1, name: 'Tony' },
  { id: 2, name: 'Steve' },
];

function App() {
  return (
    <div>
      <h1>User List</h1>
      <List<User>
        items={users}
        renderItem={(user) => <div>{user.name}</div>}
      />
    </div>
  );
}

export default App;
  • List<User> specifies that the List component will handle User objects.
  • Note that I marked the List with <User>, which is optional. For simplicity, we can just use <List ... />. Typescript will automatically get the type from items.

Example 2: List of Products

import React from 'react';
import List from './List';

interface Product {
  id: number;
  name: string;
  price: number;
}

const products: Product[] = [
  { id: 1, name: 'Laptop', price: 999 },
  { id: 2, name: 'Phone', price: 699 },
];

function App() {
  return (
    <div>
      <h1>Product List</h1>
      <List<Product>
        items={products}
        renderItem={(product) => (
          <div>
            {product.name} - ${product.price}
          </div>
        )}
      />
    </div>
  );
}

export default App;

Similar with example 1, we pass the type and items to List and tell it how to render the items. With this, we can save us a lot of duplicated code.

Conclusion

Generics in React components provide a powerful way to create flexible and type-safe components. By defining a generic component and using it with different data types, we can reduce code duplication and make our codebase easier to maintain!