DEV Community

Cover image for React: About highly customizable components - P1
Abdolah Keshtkar
Abdolah Keshtkar

Posted on • Originally published at theironside.io

React: About highly customizable components - P1

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 😈).

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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.

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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

type MyCustomComponentProps = ComponentProps<typeof MyCustomComponent>;
type ButtonProps = ComponentProps<"button">;
Enter fullscreen mode Exit fullscreen mode

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

<List
  rows={data}
  liProps={{
    style: {
      color: 'red',
    },
  }}
/>
Enter fullscreen mode Exit fullscreen mode

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:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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

Using the component will go like this:

<List
  rows={data}
  slotProps={{
    li: {
      style: {
        color: 'green',
      },
    },
    ul: {
      style: {
        padding: '12px',
      },
    },
  }}
/>
Enter fullscreen mode Exit fullscreen mode

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:

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

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

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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.

type MaybeFunc<Value, Args extends Array<unknown>> = Value | ((...args: Args) => Value)
Enter fullscreen mode Exit fullscreen mode

We can use it like so:

interface Props {
  rows: TItem[];
  slotProps?: {
    ul?: ComponentProps<'ul'>;
    li?: MaybeFunc<ComponentProps<'li'>, [TItem]>;
  };
}
Enter fullscreen mode Exit fullscreen mode

Final Code

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

Post link in my blog

Top comments (0)