Making React Components More Flexible with TypeScript Generics: 3 Examples
Tags: typescript, webdev, javascript, react
Published: 2024-03-12 13:00:00

Generic React components allow you to create a component that can be used with any data type. This is a great way to create reusable components that can be used in multiple places in your application.

There are a lot of articles and tutorials on generics in TypeScript, so I will focus on how to use generics in React components to make them more flexible and reusable.

## Example 1: Simple Generic React Component (easy to understand)

In this example, we will create a generic React component that can be used with any data type.

To clarify generics' benefits, we will create a component with data of type `T` and a render function.

### Create a Generic React Component

```typescript
// Define a generic type for the props
type Props<T> = {
  data: T
  render: (data: T) => React.ReactNode
}

// Create a generic React component
export function GenericComponent<T,>({ data, render }: Props<T>) {
  return <div>{render(data)}</div>
}
```

Here, `GenericComponent` accepts prop render, which is a function that takes data of type `T` and returns a `React.ReactNode`. This pattern, often called “render props,” gives you more control over how the data is rendered.

Next, we use `GenericComponent` with different types of data, showing how TypeScript ensures that the types are used consistently.

### Use the GenericComponent with a string

```typescript
import GenericComponent from './GenericComponent'

function RenderString() {
  return <GenericComponent<string> 
    data='Hello, world!' 
    render={(data) => <span>{data.toUpperCase()}</span>} 
  />
}
```

Here, `GenericComponent` is explicitly told to expect a string. The render function converts the string to uppercase, which is a valid operation on strings. TypeScript ensures that the operations you perform in the render prop are appropriate for the type of data passed.

### Use the GenericComponent with a custom type

```typescript
type Person = {
  name: string
  age: number
}

import GenericComponent from './GenericComponent'

function RenderPerson() {
  return (
    <GenericComponent<Person>
      data={{ name: 'Alice', age: 30 }}
      render={(data) => (
        <span>
          {data.name} is {data.age} years old
        </span>
      )}
    />
  )
}
```

In this case, TypeScript ensures that the data prop matches the type expected by the render function.

Let’s move on to a more complex example. We will create a generic component that can be used with a list of items.

### Use the GenericComponent with a List of Todos

```typescript

import { useState } from 'react'
import GenericComponent from './GenericComponent'

interface TodoItem {
  id: number
  title: string
  completed: boolean
}

export function TodoList() {
  // Initial list of todos
  const initialTodos: TodoItem[] = [
    { id: 1, title: 'Learn React', completed: false },
    { id: 2, title: 'Write blog post', completed: true },
    { id: 3, title: 'Study TypeScript', completed: false },
  ]

  // State to track todos
  const [todos, setTodos] = useState<TodoItem[]>(initialTodos)

  // Function to toggle the completion status of a todo item
  function toggleTodoCompletion(id: number) {
    const updatedTodos = todos.map((todo) => 
        (todo.id === id ? { ...todo, completed: !todo.completed } : todo))
    setTodos(updatedTodos)
  }

  // Complex render function for a list of TodoItems
  function renderTodos(todos: TodoItem[]) {
    return (
      <ul>
        {todos.map((todo) => (
          <li
            key={todo.id}
            style={{ textDecoration: todo.completed ? 'line-through' : 'none', cursor: 'pointer' }}
            onClick={() => toggleTodoCompletion(todo.id)}
          >
            {todo.title}
          </li>
        ))}
      </ul>
    )
  }

  return <GenericComponent<TodoItem[]>  
    data={todos} 
    render={renderTodos} 
  />
}
```

In this example, we create a `TodoList` component that uses `GenericComponent` to render a list of `TodoItems`. The render function is more complex, as it needs to handle a list of items. TypeScript ensures that the data prop matches the type expected by the render function.

In the next example, the benefits of generics will be more clear. We will create a generic component that fetches data from an API and displays it.

## Example 2: Generic React Component to Fetch and Display Data

In this example, we will create a generic React component that fetches data from an API and displays it. This is so often used in real-world applications that it’s a great example of how generics can make your components more flexible and reusable.


![JSONPlaceholder banner](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dv8arqmde50to3yl6h1k.png)


### Create a Generic React Component to Fetch Data

```typescript

import { useEffect, useState } from 'react'

// Define a generic type for the props
type Props<T> = {
  url: string
  render: (data: T) => React.ReactNode
}

// Create a generic React component
export function FetchAndDisplay<T,>({ url, render }: Props<T>) {
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<Error | null>(null)

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url)
        if (!response.ok) {
          throw new Error('Failed to fetch data')
        }
        const json = await response.json()
        setData(json)
      } catch (error) {
        setError(error)
      } finally {
        setLoading(false)
      }
    }
    fetchData()
  }, [url])

  if (loading) {
    return <div>Loading...</div>
  }

  if (error) {
    return <div>Error: {error.message}</div>
  }

  if (data) {
    return <div>{render(data)}</div>
  }

  return null
}
```

In this example, `FetchAndDisplay` is a generic component that accepts a URL and a render function. It uses the `fetch`to make a request to the URL and then calls the render function with the data. The component also handles loading and error states.

### Using the FetchAndDisplay component to Fetch and Display Posts

```typescript
import { FetchAndDisplay } from './FetchAndDisplay'

type Post = {
  userId: number
  id: number
  title: string
  body: string
}

function RenderPosts(posts: Post[]) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.body}</p>
        </li>
      ))}
    </ul>
  )
}

export function PostList() {
  return <FetchAndDisplay<Post[]> 
    url='https://jsonplaceholder.typicode.com/posts'  
    render={RenderPosts} 
  />
}
```

In this example, we use the `FetchAndDisplay` component to fetch a list of posts from the JSONPlaceholder API and then render them using the `RenderPosts` function. 

Having the generic component `FetchAndDisplay` allows us to fetch and display data from any API and render it in any way we want.

For example, we can use the same component to fetch and display a list of users, comments, or any other type of data.

### Using the FetchAndDisplay component to Fetch and Display Users

```typescript
import { FetchAndDisplay } from './FetchAndDisplay'

type User = {
  id: number
  name: string
  email: string
}

function RenderUsers(users: User[]) {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>
          <h3>{user.name}</h3>
          <p>{user.email}</p>
        </li>
      ))}
    </ul>
  )
}

export function UserList() {
  return <FetchAndDisplay<User[]> 
    url='https://jsonplaceholder.typicode.com/users' 
    render={RenderUsers} 
  />
}
```

Here, we use the same `FetchAndDisplay` component to fetch a list of users from the JSONPlaceholder API and then render them using the `RenderUsers` function. This is the power of generics in React components.

## Example 3: Generic React Component for Form Input

This example demonstrates how to create a generic form component that can be used with different types of form fields. 

In real-world applications, I would use a library like **Formik** or **react-hook-form** for form handling, but to demonstrate generics, we will create a simple form component from scratch.


![Form example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hqthhislta4262y6joaz.png)

First, define TypeScript types to specify the structure of your form fields and the props your generic form component will accept. This ensures type safety and helps manage the form’s state and behavior.

```typescript

type FormField = {
  name: string
  label: string
  type: 'text' | 'email' | 'password' // Extend as needed
}

type GenericFormProps<T> = {
  fields: FormField[]
  initialValues: T
  onSubmit: (values: T) => void
}
```

Next, create a functional component that takes in fields, initial values, and an `onSubmit` function.

```typescript
import { useState } from 'react'

export function GenericForm<T>({ fields, initialValues, onSubmit }: GenericFormProps<T>) {
  const [values, setValues] = useState<T>(initialValues)

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    const { name, value } = e.target
    setValues((prevValues) => ({ ...prevValues, [name]: value }))
  }

  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    onSubmit(values)
  }

  return (
    <form onSubmit={handleSubmit}>
      {fields.map((field) => (
        <div key={field.name}>
          <label htmlFor={field.name}>{field.label}</label>
          <input type={field.type} name={field.name} 
                 value={(values as any)[field.name]} 
                 onChange={handleChange} />
        </div>
      ))}
      <button type='submit'>Submit</button>
    </form>
  )
}
```

Finally, use the GenericForm component with specific form fields and initial values.

```typescript

type UserFormValues = {
  name: string
  email: string
  password: string
}

const userFormFields: FormField[] = [
  { name: 'name', label: 'Name', type: 'text' },
  { name: 'email', label: 'Email', type: 'email' },
  { name: 'password', label: 'Password', type: 'password' },
]

const initialValues: UserFormValues = {
  name: '',
  email: '',
  password: '',
}

function App() {
  const handleSubmit = (values: UserFormValues) => {
    console.log('Form Submitted', values)
  }

  return (
    <div>
      <h1>User Registration</h1>
      <GenericForm 
        fields={userFormFields} 
        initialValues={initialValues} 
        onSubmit={handleSubmit} 
      />
    </div>
  )
}
```

In this example, without using generics, you would have to create a separate form component for each type of form. With generics, you can create a single form component that can be used with any type of form fields.

## Bonus Example: Table Component with Generics

This example is taken from the video:

{% embed https://www.youtube.com/watch?v=zqz-lEHjJr4&ab_channel=MattPocock %}

In the example, the table component is made generic by adding a type parameter called `TRow`. This `TRow` parameter represents the type of data that will be in each row of the table.

The `Table` component takes in an array of rows and a `renderRow` function. The `renderRow` function is called for each row in the array and is responsible for rendering the row.

```typescript

const Table = <TRow extends Record<string, any>>(props: {
  rows: TRow[];
  renderRow: React.FC<TRow>;
}) => {
  return (
    <table>
      <tbody>
        {props.rows.map((row) => (
          <props.renderRow {...row} />
        ))}
      </tbody>
    </table>
  )
}
```

In this example, the `Table` component takes in an array of rows and a `renderRow` function. The `renderRow` function is called for each row in the array and is responsible for rendering the row.

```typescript
<Table
  rows={[
    { name: 'Alice', age: 30 },
    { name: 'Bob', age: 40 },
  ]}
  renderRow={({ name, age }) => (
    <tr>
      <td>{name}</td>
      <td>{age}</td>
    </tr>
  )}
/>
```

In this example, the `Table` component is used to render a table with two rows, each containing a name and an age. The `TRow` type parameter ensures that the rows array and the renderRow function are used consistently.

## Conclusion

Generics are a powerful feature of TypeScript that can make your React components more flexible and reusable. They allow you to create components that can be used with any data type, which is especially useful in real-world applications where you often need to work with different types of data.

I hope this article has given you a good understanding of how to use generics in React components and how they can make your components more flexible and reusable. If you have any questions or feedback, please feel free to leave a comment below.

## Helpful Resources

* [TypeScript Generics](https://www.typescriptlang.org/docs/handbook/2/generics.html)
    
* [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/basic_type_example)
    
* [Matt Pocock's channel on YouTube](https://www.youtube.com/@mattpocockuk)

Check out my other articles on TypeScript:
- [TypeScript Index Signatures: 4 Examples Type-Safe Dynamic Objects](https://dev.to/alexefimenko/typescript-index-signatures-4-examples-type-safe-dynamic-objects-554o)
- [5 Resources Each TypeScript Developer Should Know About](https://dev.to/alexefimenko/5-resources-each-typescript-developer-should-know-about-30m4)
- [TypeScript Enums: 5 Real-World Use Cases](https://dev.to/alexefimenko/typescript-enums-5-real-world-use-cases-4idk)
    
This article was originally [posted on Medium](https://medium.com/@alexefimenko/3-examples-of-typescript-generic-react-components-3b186d9d5402).