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>
}
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>}
/>
}
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>
)}
/>
)
}
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}
/>
}
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.
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
}
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
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
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.
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
}
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>
)
}
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>
)
}
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>
)
}
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>
)}
/>
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:
- TypeScript Index Signatures: 4 Examples Type-Safe Dynamic Objects
- 5 Resources Each TypeScript Developer Should Know About
- TypeScript Enums: 5 Real-World Use Cases
This article was originally posted on Medium.
Top comments (0)