React: About highly customizable components - P1
Tags: react, javascript, typescript
Published: 2024-03-29 21:16:05



## Overview

Tables and Lists are common examples of components that should be reusable and customizable.

## The Problem

Let's say we have the following component and want to make it customizable (by over-engineering it 😈).

```jsx
import { FC } from 'react';

interface Props {
  rows: { id: string; name: string }[];
}

const List: FC<Props> = ({ rows }) => {
  return (
    <ul>
      {rows.map((row) => (
        <li key={row.id}>{row.name}</li>
      ))}
    </ul>
  );
};
```

**What are the problems?**
1. We want to pass extra props to the `li` and `ul` tags.
2. We want to use other components instead of the `li` and `ul` tags (+ props should adapt to the type of the props of these components).

In this post, we will be addressing the first problem. In the next post, we'll tackle the second one.

## Passing Extra Props to an "li"

It is actually quite straightforward; you may have already done it multiple times. We can accept a prop, spread it, and pass it to the `li`.

```jsx
import { ComponentProps, FC } from 'react';

interface Props {
  rows: { id: string; name: string }[];
  liProps?: ComponentProps<'li'>;
}

const List: FC<Props> = ({ rows, liProps }) => {
  return (
    <ul>
      {rows.map((row) => (
        <li key={row.id} {...liProps}>
          {row.name}
        </li>
      ))}
    </ul>
  );
};
```

We can use the `ComponentProps` type from React to get the props type of any component we pass to it.

```ts
type MyCustomComponentProps = ComponentProps<typeof MyCustomComponent>;
type ButtonProps = ComponentProps<"button">;
```

Let's move forward. Now we can pass props to the `li` tag. We also get auto-complete (with love to Typing 😏).

```jsx
<List
  rows={data}
  liProps={{
    style: {
      color: 'red',
    },
  }}
/>
```

## Passing Extra Props to a "ul"

Try to do it yourself in Stackblitz or Codesandbox. If you are too lazy to do it, then let's go.

Here we will do more steps, like having better naming and grouping, but first let's do it the simple way:

```jsx
import { ComponentProps, FC } from 'react';

interface Props {
  rows: { id: string; name: string }[];
  liProps?: ComponentProps<'li'>;
  ulProps?: ComponentProps<'ul'>;
}

const List: FC<Props> = ({ rows, ulProps, liProps }) => {
  return (
    <ul {...ulProps}>
      {rows.map((row) => (
        <li key={row.id} {...liProps}>
          {row.name}
        </li>
      ))}
    </ul>
  );
};
```

So, I believe it's fine to have one prop to get the props of others, but it may get crowded in the parent component, so I like to group them, something like this:

```jsx
import { ComponentProps, FC } from 'react';

interface Props {
  rows: { id: string; name: string }[];
  slotProps?: {
    ul?: ComponentProps<'ul'>;
    li?: ComponentProps<'li'>;
  };
}

const List: FC<Props> = ({ rows, slotProps }) => {
  return (
    <ul {...slotProps?.ul}>
      {rows.map((row) => (
        <li key={row.id} {...slotProps?.li}>
          {row.name}
        </li>
      ))}
    </ul>
  );
};
```

I use `slotProps`, inspired by MUI, but feel free to use other names.

Using the component will go like this:

```jsx
<List
  rows={data}
  slotProps={{
    li: {
      style: {
        color: 'green',
      },
    },
    ul: {
      style: {
        padding: '12px',
      },
    },
  }}
/>
```

It's pretty cool, right? No? "Wtf, you said no?" Wait dude, so what if we want to pass onClick to `li` and we want to do an alert with the name of the item on click (or maybe navigate or anything).

So here is where I go into a fight with Performance bros, but let's ignore them for so many seconds and let's find a solution.

I mean, of course, I already have a solution for it; otherwise, I wouldn't write this post 😊.

So the problem is we want to pass different props based on the value of the item. What we can do is accept a callback that returns the props, and we pass the item to it, something that can be used like this:

```jsx
<List
  rows={data}
  slotProps={{
    li: ({ name }) => ({
      style: {
        color: 'green',
      },
      onClick: () => alert(name),
    }),
    ul: {
      style: {
        padding: '12px',
      },
    },
  }}
/>
```

In the above example, we accepted a function, passed the row, and got the props. It's actually simple:

```jsx
import { ComponentProps, FC } from 'react';

interface TItem {
  id: string;
  name: string;
}

interface Props {
  rows: TItem[];
  slotProps?: {
    ul?: ComponentProps<'ul'>;
    li?: ComponentProps<'li'> | ((row: TItem) => ComponentProps<'li'>);
  };
}

const List: FC<Props> = ({ rows, slotProps }) => {
  return (
    <ul {...slotProps?.ul}>
      {rows.map((row) => (
        <li
          key={row.id}
          {...(typeof slotProps?.li === 'function'
            ? slotProps?.li(row)
            : slotProps?.li)}
        >
          {row.name}
        </li>
      ))}
    </ul>
  );
};
```

Or if we want to be cooler with typing, we can write a cool generic.

Basically, this generic should accept a value type and args type, then return the value or a function that accepts the args type and returns the value.

```ts
type MaybeFunc<Value, Args extends Array<unknown>> = Value | ((...args: Args) => Value)
```

We can use it like so:

```ts
interface Props {
  rows: TItem[];
  slotProps?: {
    ul?: ComponentProps<'ul'>;
    li?: MaybeFunc<ComponentProps<'li'>, [TItem]>;
  };
}
```

## Final Code

<iframe style="width: 100%; height: 500px; border: 0" src="https://stackblitz.com/edit/vitejs-vite-u3u8uv?ctl=1&embed=1&file=src%2FApp.tsx&hideExplorer=1&hideNavigation=1" />

**Note:** Of course, there are different patterns like component composition that can be used, but trust me, bro!

[Post link in my blog](https://theironside.io/react-about-highly-customizable-components/)