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>
);
};
What are the problems?
- We want to pass extra props to the
li
andul
tags. - We want to use other components instead of the
li
andul
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>
);
};
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">;
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',
},
}}
/>
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>
);
};
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>
);
};
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',
},
},
}}
/>
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',
},
},
}}
/>
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>
);
};
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)
We can use it like so:
interface Props {
rows: TItem[];
slotProps?: {
ul?: ComponentProps<'ul'>;
li?: MaybeFunc<ComponentProps<'li'>, [TItem]>;
};
}
Final Code
Note: Of course, there are different patterns like component composition that can be used, but trust me, bro!
Top comments (0)