37
loading...
This website collects cookies to deliver better user experience
List
component. We will use React and TypeScript for building this example. Let's get started!List
component that can be used in different parts of the product.List
component to our component library. It's nothing fancy! We just need a list of items like this:List
component should be opinionated about how the items are rendered to ensure consistency across the product. She decides to make the List
component responsible for rendering the items. In her vision, the items are sent to the List
as a prop and the List
takes care of rendering them. She starts building the List
component with an interface like this:interface ListItem {
title: string;
description: string;
}
interface ListProps {
items: ListItem[];
}
List
component that can be used like this:const items = [
{
title: "item 1",
description: "description for item 1",
},
{
title: "item 2",
description: "description for item 2",
},
{
title: "item 3",
description: "description for item 3",
},
];
...
<List
items={items}
/>
List
component and decides to add an icon property to each item:interface ListItem {
icon: IconName;
title: string;
description: string;
}
interface ListProps {
items: ListItem[];
}
List
to receive an icon for each item. But that's not a big deal.const items = [
{
icon: "icon1",
title: "item 1",
description: "description for item 1",
},
{
icon: "icon2",
title: "item 2",
description: "description for item 2",
},
{
icon: "icon3",
title: "item 3",
description: "description for item 3",
},
];
...
<List
items={items}
/>
List
component is now in the wild and people are happily using it. But Destin is thinking of new use cases for the component.List
component. There are some lists that we would like to have an action button for each item. In some other lists, we would like to have some extra details text in place of the button:List
component complex but let me see what I can do.title
) and some are unique to each item type. She decides to extract the shared properties into a new interface named ListItemBase
and define ActionListItem
and ExtraDetailListItem
that extend the ListItemBase
:interface ListItemBase {
icon: IconName;
title: string;
description: string;
}
interface ActionListItem extends BaseListItem {
type: "ListItemWithAction";
action: {
label: string;
onClick(event: React.MouseEvent<HTMLButtonElement>): void;
};
}
interface ExtraDetailListItem extends BaseListItem {
type: "ListItemWithExtraDetail";
extraDetail: string;
}
items
in the ListProps
now have a new type:interface ListProps {
items: (ActionListItem | ExtraDetailListItem)[];
}
List
component that decides whether to render an ActionListItem
or ExtraDetailListItem
.List
component to support the two new types of list items.List
component can be used for rendering a list of messages. He presents the new use case to Enna.List
component handle this?List
component to handle this use case but it will add a lot of complexity to the component.List
ensures there's a unified way of rendering items which will provide the consistency we would like to have across our products. But with every single change to the List
, we increase the chance of regression for all instances of the List
. No need to mention that we are also adding more and more complexity to the List
which makes its maintenance harder. So what can we do?List
component. In the initial version, the List
component had two separate responsibilities:List
component, but how each item gets rendered could have been extracted into its own set of components.List
component. The list items can change depending on the feature we are building and the customer's needs. The requirement for the list itself would not generally change from feature to feature. So the list and list items have different reasons for changing. This means they are different concerns.List
component, how can we separate them? Compound Components are the way to accomplish this. The List
component can accept its items as children like this:<List>
{items.map(({ icon, title, description }) => {
<ListItem {...{ icon, title, description }} />;
})}
</List>
ListItem
would not alter the code in the List
component. This helps with less regression over timeListItem
to be able to handle messages. But wait! Do message items have different reasons for changing than the generic ListItem
? Yes! They are representing two different types of information that can have different reasons for change. Hence our message item is a new concern. We can create a new component for the MessageItem
:<List>
{messages.map((message) => {
<MessageItem
thumbnail={messages.thumbnail}
sender={message.sender}
content={message.content}
sentAt={message.sentAt}
hasBeenRead={message.hasBeenRead}
/>;
})}
</List>
List
component to a variety of use cases without touching anything in the List
component!List
component concerns using the Compound Component pattern helps embracing future changes more easily without causing regression.List
component into smaller components that can be passed as children for the List
. This made the component less complex, easier to maintain, and flexible to future changes. But now we created a new problem! Any component can be passed as children to the List
and we lost control over which types of items we render in the list.List
component, this might feel like we can't enforce the design system's opinions on the List
component. In order to enforce those opinions, we can check the type of each child and ensure they are aligned with the opinion of our design system. Depending on how strict you want to be, you can show a warning message or even not render the items that are not accepted by the design system:const ACCEPTED_LIST_ITEMS = [ListItem, MessageListItem];
function List({children}) {
...
return React.Children.map(children, (child) => {
if (ACCEPTED_LIST_ITEMS.includes(child)) {
return child
} else {
console.warn("The List can't render this type of item")
}
})
}
List
component is firm in allowing only certain types of items.