Design Challenge 006: Breaking Down a God Component
If you’ve been following along, you might remember the case we discussed about building a flexible template selector component in Design Challenge 001 and the solution we shared here.
In essence, this component allows users to select an item from a list of templates—such as form templates or workflow templates—helping them quickly set up predefined forms (like feedback forms, leave requests, etc.). This saves users from having to manually select each input field and validation rule repeatedly.
Below is an example screen that demonstrates some features of such a selector. Let’s call it ItemSelector for now.
It has a header at the top, a search input, and a grouping mechanism to categorize the items (e.g., HR forms, analysis forms, administration forms, etc.), followed by the list of items. At the bottom, there are buttons for cases where a selection requires further action.
The Problem
Initially, the component was very simple, requiring just a few parameters to display items, as shown below:
const ItemSelector = ({ items }) => {
// render the items
};
At first, this worked perfectly. It was straightforward and had no major issues. But as new requirements emerged, things started to change.
For example, when the data wasn’t ready yet, it became clear that showing a skeleton loader would improve the user experience. So, we updated the component to handle a loading state:
const ItemSelector = ({ items, isLoading }) => {
// render the items
if (isLoading) {
return <ItemSelectorSkeleton />;
}
};
Next, another feature was requested: the ability to customize how the categories on the left were sorted. To support this, we added a sorting function:
const ItemSelector = ({ items, isLoading, onSortGroupItem }) => {
// render the items
if (isLoading) {
return <ItemSelectorSkeleton />;
}
const sortedItems = onSortGroupItem(items);
// render sorted items
};
Soon after, a request came in to allow customisation of the header (as we saw in the original Design Challenge 001), and the component evolved again:
const ItemSelector = ({ items, isLoading, onSortGroupItem, header }) => {
// render the items
if (isLoading) {
return <ItemSelectorSkeleton />;
}
const sortedItems = onSortGroupItem(items);
// render with custom header
};
Before long, the component became more complex, accumulating various props and growing into what’s commonly referred to as a God component:
const ItemSelector = ({
items,
isLoading,
hasError,
customEmptyState,
onSortGroupItem,
header,
onPrevious,
onNext,
customErrorState,
onSearch,
customFilterComponent,
onTileClick,
defaultSelectedGroup,
//...
}) => {
// render the items
if (isLoading) {
return <ItemSelectorSkeleton />;
}
if (hasError) {
return customErrorState || <DefaultErrorState />;
}
const sortedItems = onSortGroupItem(items);
// render sorted items
};
The result was a bloated component packed with functionality and options, making it harder to maintain, extend, and even test.
UI Variations
This component isn’t even at its worst yet, as more users are starting to adopt the ItemSelector, each with unique needs. Some require a different header layout, others don't need a footer at all.
Here’s another variation, where the tile view needs to show an image thumbnail instead of a text-based tile:
While most of the features remain similar, the UI looks entirely different. This is a common scenario in real-world projects.
As the component grows, accommodating new changes becomes more challenging. For example, what if the user doesn’t need a searchable list and just wants a simple card layout?
A quick fix might be to add a showCardOnly
prop and conditionally render the components based on that flag:
const ItemSelector = ({ items, showCardOnly }) => {
const [selectedCategory] = useState();
const categories = buildCategoryByItems(items);
// ... other logic
return (
<>
{!showCardOnly && (
<>
<SearchInput />
<CategoryList categories={categories} selected={selectedCategory} />
</>
)}
<CardList items={items} />
</>
);
};
But this just adds more props and conditional logic to the already overloaded ItemSelector, leading to maintenance headaches and a higher risk of introducing bugs.
The Challenge
Now it’s your turn to think of ways to simplify this overly complex ItemSelector component. The main goal is to break it down into manageable, reusable components that are easier to extend, modify, and test. This will help speed up development and improve overall maintainability.
As always, I’ll share my solution in the next issue, but I encourage you to take some time to think about it. Write some code, extract use cases from your own projects, and feel free to share your thoughts with me.