How to Create Generic Functional Components in React (Typescript)
Jasser Mark Arioste
Hello, hustler! In this tutorial, you'll learn how to create generic functional components in react.
Why Create Generic React Components? #
By creating generic components, we maximize the reusability of our code so that it caters to different types while still retaining its core functionality.
What do I mean by this?
Suppose that on one page of your app, you have a <UserList />
component that can sort by age
or name
. On another page, you have an <OrderList/>
component that can sort by amount
or createdDate
. Both lists have the same sort function, but they are defined as two separate components since the properties of users
are not the same as orders
.
Without using generics, it's impossible to create a generic list component with a sort functionality.
In this tutorial, we'll create a generic list component to create one component for the two lists.
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.
Since functional components in react are pretty much the same as functions in Typescript.
Defining a generic named function in Typescript
To define a generic named function, we need to add a generic type parameter <T> after the function name.
/** * Transforms any value into an array of that value type */ export function toArray<T>(value: T): T[] { return [value]; } toArray("hello") //returns a string[] toArray(5); //returns a number[] type Person = { name: string; }; const person: Person = {name: "John Doe"} toArray(person) //returns a Person[]
123456789101112131415
The <T> can be any descriptive name that fits your needs for example:
export function toArray<TValue>(value: TValue): TValue[] { return [value]; }
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<TValue extends WithFullName>(value: TValue): TValue[] { return [value]; } toArray({fullName: "JohnDoe"}) // 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 = <TValue>(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<T> = { value: T; }; export default function ComponentA<T>(props: Props<T>) { return <pre>{JSON.stringify(props.value, null, 4)}</pre>; }
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.
1. ) Using a trailing comma:
// components/TrailingComma.tsx type Props<T> = { value: T; }; // notice the trailing comma after <T const TrailingComma = <T,>(props: Props<T>) => { return <pre>{JSON.stringify(props.value, null, 4)}</pre>; }; export default TrailingComma;
123456789
Here we use a trailing comma in <T,>
to clarify that it is a type parameter and not a JSX.Element
. If we don't use the trailing comma, it will confuse the compiler and will result in a type error:
2. Using the extends
keyword
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<T> = { value: T; }; const ExtendsUnknown = <T extends unknown>(props: Props<T>) => { return <pre>{JSON.stringify(props.value, null, 4)}</pre>; }; export default ExtendsUnknown;
12345678
We use <T extends unknown>
to clarify that this is a type parameter and not a JSX.Element
. We use unknown
since we're not really expecting a specific type for T.
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 <SortedList/>
component that accepts items
props. Each item
can have any field. It also accepts a sortBy
prop that is restricted to the actual properties of each item.
First, create the file components/SortedList.tsx
, and copy the following code:
// components/SortedList.tsx type Props<T extends Object> = { 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<T extends Object>({ items, sortBy, sortOrder, renderItem, }: Props<T>) { // 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<T extends Object>({ items, sortBy, sortOrder, renderItem, }: Props<T>) { // sort the items const sortedItems = [...items].sort(); // render the items return <>{sortedItems.map(renderItem)}</>; }
12345678910111213
Next, let's add actual sorting implementation:
// components/SortedList.tsx ... export default function SortedList<T extends Object>({ items, sortBy, sortOrder, renderItem, }: Props<T>) { // 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 <>{sortedItems.map(renderItem)}</>; }
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 ( <div> <SortedList items={data} renderItem={(item, index) => { return ( <div key={index}> {item.name} - {item.age} </div> ); }} sortBy="age" sortOrder={1} /> </div> ); }; export default users;
12345678910111213141516171819202122232425262728293031323334353637383940
Notice that renderItem
and sortBy
is fully-typed and that it accepts only the properties of the data.
Full Code #
If you want to check the full code, it can be accessed at GitHub: React Generic Components Tutorial
Conclusion #
We learned how to create generic components in react and we also learned the different ways to define generic components in arrow functions. We applied what we learned by creating a generic SortedList
component. For the next steps, try to see how you can apply generics in your web application.
Resources #
Credits: Image by Samina Kousar from Pixabay