- How to Create Generic Functional Components in React (Typescript)
- Defining Generic Functions in Typescript #
- Defining a generic named function in Typescript
- Defining a generic arrow function in Typescript
- Defining Generic Components in React #
- Defining a generic named Function Component
- Defining a Generic Arrow Function Component
- Example Component #
- Defining the use case
- Introducing generic components
- Defining a generic component
- Implementing a fully working table
- Putting it all together
- Summary
- Sign up for newsletter
- Recent posts
- Series
- Marcin Wanago
How to Create Generic Functional Components in React (Typescript)
By creating generic components, we maximize the reusability of our code so that it caters to different types while still retaining its core functionality.
Suppose that on one page of your app, you have a
Without using generics, it’s impossible to create a generic list component with a sort functionality.
Defining Generic Functions in Typescript #
First, let’s examine how to define generic functions in Typescript. We’ll only tackle the basics of generics here.
Defining a generic named function in Typescript
/** * Transforms any value into an array of that value type */ export function toArray(value: T): T[] < return [value]; >toArray("hello") //returns a string[] toArray(5); //returns a number[] type Person = < name: string; >; const person: Person = toArray(person) //returns a Person[]
123456789101112131415
export function toArray(value: TValue): TValue[]123
If you’re expecting a certain type or interface , you can use the extends keyword. Let’s say you’re expecting an object with a fullName property:
interface WithFullName < fullName: string >export function toArray(value: TValue): TValue[] < return [value]; >toArray() // good. no type error toArray("test") //type error. The argument of type 'string' is not assignable to parameter of type 'WithFullName'
123456789
Defining a generic arrow function in Typescript
To define a generic arrow function in typescript, we add a generic parameter before the left parenthesis:
export const toArray = (value: TValue): TValue[] => < return [value]; >; //usage is the same as above toArray(5) // returns [5] toArray("hello") // returns ["hello"]
123456
Defining Generic Components in React #
Since we now know how to define generic functions in typescript, let’s define some generic react components. It’s pretty similar to typescript.
Defining a generic named Function Component
// components/ComponentA.tsx type Props = < value: T; >; export default function ComponentA(props: Props) < return
; >12345678
Here we define a simple generic functional component that accepts any type of value and renders it as a string. Notice that it’s the same as the typescript function definition in the first example.
Defining a Generic Arrow Function Component
In .tsx files, there are two ways to define the generic arrow function component. To avoid ambiguity between type parameters and JSX.Element , there’s somewhat of an extra step when defining generic components.
// components/TrailingComma.tsx type Props = < value: T; >; // notice the trailing comma after (props: Props) => < return
; >; export default TrailingComma;123456789
Here we use a trailing comma in
If you don’t prefer the syntax of trailing commas, we can use the extends keyword which has the same purpose:
// components/ExtendsUknown.tsx type Props = < value: T; >; const ExtendsUnknown = (props: Props) => < return
; >; export default ExtendsUnknown;12345678
We use
Example Component #
All right, you now know how to define generic functional components in React, but here let me know you a concrete example. Suppose you need to create a generic
// components/SortedList.tsx type Props = < items: T[]; sortBy: keyof T; //restricts the sortBy prop to the properties of T sortOrder: 1 | -1; renderItem(item: T, index: number): ReactNode; >; export default function SortedList(< items, sortBy, sortOrder, renderItem, >: Props) < // render the items return <>SortedList>; >
12345678910111213141516
Above, we’re just defining the skeleton of the component. Next, let’s render the items by using the renderItem function.
// components/SortedList.tsx . export default function SortedList(< items, sortBy, sortOrder, renderItem, >: Props) < // sort the items const sortedItems = [. items].sort(); // render the items return <>>; >
12345678910111213
// components/SortedList.tsx . export default function SortedList(< items, sortBy, sortOrder, renderItem, >: Props) < // sort the items const sortedItems = [. items].sort((a, b) => < const primitiveType = typeof a[sortBy]; let result = 0; switch (primitiveType) < case "bigint": case "number": result = (a[sortBy] as number) - (b[sortBy] as number); break; case "string": result = (a[sortBy] as string).localeCompare(b[sortBy] as string); break; default: break; >return result * sortOrder; >); // render the items return <>>; >
1234567891011121314151617181920212223242526272829
That’s it! To use this component, we just have to supply it with the correct props. Here’s an example of rendering a sorted list of users:
// pages/users.tsx import SortedList from "../components/SortedList"; const users = () => < const data = [ < name: "John", age: 16, >, < name: "Lebron", age: 38, >, < name: "Jordan", age: 23, >, < name: "Zach", age: 27, >, ]; return ( renderItem= <(item, index) =>< return ( > - ); >> sortBy="age" sortOrder= /> ); >; export default users;12345678910111213141516171819202122232425262728293031323334353637383940 Notice that renderItem and sortBy is fully-typed and that it accepts only the properties of the data.
One of the qualities of our code that we should aim for is reusability. Also, following the Don’t Repeat Yourself principle makes our code more elegant. In this article, we explore writing functional React components with TypeScript using generic props. By doing so, we can create reusable and flexible components.
If you would like to revisit the basics of generics first, check out TypeScript Generics. Discussing naming conventions
Defining the use case
One of the things that contribute to good design is consistency. Implementing it means having lots of similar parts of the application. One of the components that we encounter is a table. Let’s create an example of such. For starters, we make it in a way that it displays a certain entity – posts.
The above code is pretty straightforward. Our PostsTable takes an array of posts and displays all of them. Let’s fetch the posts and provide our component with them.
The header always displays a predefined set of properties.
The same thing with the row – it displays just the title and the body.
While the above PostsTable component is reusable to some extent, it is not very flexible. We can just use it to display posts.
The fact is that there are high chances that we need more tables in our application, not only for posts. Creating a separate one for every type of entity is a solution, but not the most refined one.
Introducing generic components
First, let’s define the requirements for our generic table component. It needs to:
- accept an array of entities of any type,
- process an array of properties that we want to display in a table.
Taking all of the above into consideration we can specify our Props interface.
First, we have an array of objects . Second, we have an array of properties . Our property consists of a key and a title .
A crucial thing is that the key needs to match the properties of the object. To do so, we use the keyof keyword to create a union of string literal types. If you want to learn more, check out More advanced types with TypeScript generics
To better grasp the concept, this is an example of props that implement the above interface:
Defining a generic component
In all of the above examples, we’ve used the generic FunctionComponent provided by React. Unfortunately, it would be rather troublesome to use it with generic interfaces. What we can do is to type the props directly.
If we look into the FunctionComponent interface, it uses PropsWithChildren , and we can also make use of that.
To type our component, we can choose one of the two approaches. First would be to use an arrow function:
The trailing comma in < ObjectType , >is added due to contraints of the . tsx file extension. You can read more in the TypeScript Generics. Discussing naming conventions
The second approach is to use a more classic function:
The latter looks a bit cleaner, in my opinion. Therefore, I will use it in all of the below examples.
In our Table component, we want to iterate through all of the objects. To display a row for every object, we need to pass the key prop. The most suitable property would be id. To make sure that our objects have it, we can add a constraint.
Now we can be sure that the ObjectType contains the id.
Implementing a fully working table
Our Table needs a row. It is also generic and accepts a single object and an array of properties.
The Head , on the other hand, does not need to be generic. We provide it just with a list of properties.
A thing worth pointing out is that the key above is of type number | symbol | string . The above is because the key of an object by default is of the type number | symbol | string .
To change the above behavior, we could force the keys to be of a string type by using keyof ObjectType & string .
Alternatively, we could also use the keyofStringsOnly option when compiling.
Putting it all together
Once we’ve defined all the pieces, we can put them together.
Even though the implementation of the above component might seem rather complex, its usage is very straightforward.
The TypeScript compiler understands that the ObjectType above is a post. Due to that, we can only provide properties that can be found in the Post interface.
On the other hand, we could make the ObjectType explicit by providing the interface.
Summary
Thanks to all of the above work, we now have a reusable Table component that we can use to display any entity. By doing so, we aim for reusability and make following the DRY principle easier. You can go ahead and implement more features in our Table . Thanks to it being generic, we can have them all across our application.
Which part did you found to be difficult to understand? Maybe it is worth digging deeper into it.
great write up. this finally made components with generic types click for me. thank you
Thanks! I was trying to figure out the syntax for generics with an arrow function, but indeed the classic function syntax is much cleaner.
nice, but it makes no sense – Cannot find name ‘ObjectType’
Is it possible to build a generic React component with two different data types? By setting up Props object with TWO datatypes, I can instantiate the component with two separate objects?
Could you share how to do this with the table as an arrow function?
Sign up for newsletter
Recent posts
Series
Marcin Wanago
We use cookies on our website. We would like to ask for your consent to store them on your device. We will not run optional cookies until you enable them. If you want to know more about how cookies work, please visit our Privacy Policy page.
These cookies are necessary for the proper display and operation of the website. Blocking them may cause errors or malfunctions.
The technical storage or access is necessary for the legitimate purpose of storing preferences that are not requested by the subscriber or user.
The technical storage or access that is used exclusively for statistical purposes. As part of our website, we use analytical tools, such as Google Analytics, which allow us to verify website traffic. These tools may require the use of cookies.
These are cookies responsible for conducting advertising and marketing activities — consent to their use will allow us to direct advertising to you, based on your previous activities on our website.
To provide the best experiences, we use technologies like cookies to store and/or access device information. Consenting to these technologies will allow us to process data such as browsing behavior or unique IDs on this site. Not consenting or withdrawing consent, may adversely affect certain features and functions.
These cookies are necessary for the proper display and operation of the website. Blocking them may cause errors or malfunctions.
The technical storage or access is necessary for the legitimate purpose of storing preferences that are not requested by the subscriber or user.
The technical storage or access that is used exclusively for statistical purposes. As part of our website, we use analytical tools, such as Google Analytics, which allow us to verify website traffic. These tools may require the use of cookies.
These are cookies responsible for conducting advertising and marketing activities — consent to their use will allow us to direct advertising to you, based on your previous activities on our website.