ReactHustle

How to Create Generic Functional Components in React (Typescript)

Jasser Mark Arioste

Jasser Mark Arioste

How to Create Generic Functional Components in React (Typescript)

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:

No trailing comma error in react generic component

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.

Generic Sorted List Component in React

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

Share this post!

Related Posts