DEV Community

Cover image for Making React Components More Flexible with TypeScript Generics: 3 Examples
Alex
Alex

Posted on • Edited on • Originally published at Medium

Making React Components More Flexible with TypeScript Generics: 3 Examples

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

// 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>
}
Enter fullscreen mode Exit fullscreen mode

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

import GenericComponent from './GenericComponent'

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

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

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>
      )}
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

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


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} 
  />
}
Enter fullscreen mode Exit fullscreen mode

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

Create a Generic React Component to Fetch Data


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
}
Enter fullscreen mode Exit fullscreen mode

In this example, FetchAndDisplay is a generic component that accepts a URL and a render function. It uses the fetchto 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

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} 
  />
}
Enter fullscreen mode Exit fullscreen mode

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

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} 
  />
}
Enter fullscreen mode Exit fullscreen mode

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

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.


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

type GenericFormProps<T> = {
  fields: FormField[]
  initialValues: T
  onSubmit: (values: T) => void
}
Enter fullscreen mode Exit fullscreen mode

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

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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


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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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:

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.


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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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

Check out my other articles on TypeScript:

This article was originally posted on Medium.

Top comments (0)