Wrong Component Composition in React.js Projects [Case Study on Why Software Projects Fail]
In this case study, I’m going to show you why software (React.js) projects fail.
Specifically, I want to focus on code structure in React.js projects. I will show you examples from real projects where component composition wasn’t used correctly and it took a lot of time to fix it.
You might be wondering:
What is so special about component composition? This concept is easy to understand and it looks straightforward from React.js docs.
Component composition is the essential part of code bases in React projects and there are several approaches on how to use it. Unfortunately, even senior React.js developers don’t use it correctly in complex use cases. For this reason, it makes projects hard to maintain, it takes a lot of time to fix it and add new features.
I came across projects where even senior developers didn’t know about this issue in their code bases. CTOs decide to look for new React developers because the current team is struggling to implement new features. The truth is that, instead of hiring new React developers, the project needs to be fixed to make the code base more flexible.
Different ways of components composition
I assume you already know how to use component composition in React projects. Let’s say we have an App
component that consists of its layouts elements.
function App() {
return (
<>
<h1>Demo App</h1>
<main>Content</main>
<footer>© 2022 Copyright</footer>
</>
);
}
We can implement layout by composition of it’s React components.
function Header({ children }) {
return <h1>{children}</h1>;
}
function Body({ children }) {
return <main>{children}</main>;
}
function Footer({ children }) {
return <footer>{children}</footer>;
}
function App() {
return (
<>
<Header>Demo App</Header>
<Body>Content</Body>
<Footer>© 2022 Copyright</Footer>
</>
);
}
We can create Layout
component and define it’s content by passing props.
function Layout({ header, content, footer }) {
return (
<>
<h1>{header}</h1>
<main>{content}</main>
<footer>{footer}</footer>
</>
);
}
function App() {
return (
<Layout header="Demo App" content="Content" footer="© 2022 Copyright" />
);
}
We can make Layout
component more flexible to be able to pass whatever content we need through children
props.
function Layout({ children }) {
return (
<>
{children}
</>
);
}
function App() {
return (
<Layout>
<h1>Demo App</h1>
<main>Content</main>
<footer>© 2022 Copyright</footer>
</Layout>
);
}
We can combine these approaches if we have a lot of similar layouts with some exceptions:
function Layout({ header, content, footer, children }) {
return (
<>
{ header && <h1>{header}</h1> }
{ content && <main>{content}</main> }
{ footer && <footer>{footer}</footer> }
{ children }
</>
);
}
function App() {
return (
<Layout header="Demo App" content="Content" footer="© 2022 Copyright" />
);
}
In the code snippets above I wanted to demonstrate there are a lot of ways to implement component composition. In simple use cases, it doesn’t matter. I just wanted to point out we have a lot of choices.
Wrong component composition examples
Now, I want to show you examples of components composition from real projects. We will go through complex use cases, where composition wasn’t used correctly. It took a lot of time and effort to refactor the existing code base and we will discuss how to avoid that.
Dashboard application
Let’s say we want to implement a dashboard application that consists of widgets. All widgets have similar layout and structure, but they have different data. Each widget represents data from a specific platform.
We can implement our <Widget />
component by creating <WidgetHeader />
, <WidgetBody />
and <WidgetFooter />
. Then <WidgetBody />
can consist of row components <WidgetRow />
and also a modal popup <WidgetModal />
.
If we translate the above structure into components, the implementation of Widget.tsx
looks like this:
function Widget({ widgetProps }) {
const {
widgetHeaderParams,
widgetBodyParams,
widgetFooterParams,
widgetModalParams,
children,
} = widgetProps;
return (
<div>
<WidgetHeader {...widgetHeaderParams} />
<WidgetBody {...widgetBodyParams} {...widgetModalParams}>
{children}
</WidgetBody>
<WidgetFooter {...widgetFooterParams} />
</div>
);
}
Now, the dev team decided to implement a <WidgetTemplate />
component with useWidgetTemplate
hook. This hook is a single robust function that is responsible for fetching the widget’s data, its formatting and passing props to Widget’s component.
function WidgetTemplate({ widgetTemplate }) {
const { widgetProps, rows } = useWidgetTemplate(widgetTemplate);
return (
<div>
<Widget widgetProps={widgetProps}>
{rows.map((row) => (
<WidgetRow {...row} />
))}
</Widget>
</div>
);
}
Now, let’s say we want to implement widget for Asana platform. We use widgetConfig
as a configuration object that we pass to the WidgetTemplate
. Don’t worry if some attributes in widgetConfig
don’t make sense. The purpose of this demonstration is to show that this object can have a lot of attributes.
function AsanaWidget() {
const widgetConfig = {
id: "ASANA_WIDGET",
platformId: PLATFORM_IDS.ASANA,
commonProps: {
dataFetchers: [
{
routeName: API.ASANA.GET.ROWS,
},
],
formatterFunctionName: AsanaFormattingFunction.formatDataToRows,
rowsAreEditable: false,
},
header: {
title: "Asana data",
iconPath: [PLATFORM_IDS.ASANA, IconSectionNames.SmallLogo],
menuOptions: [MenuOptionIds.Refresh, MenuOptionIds.Remove],
customizeSections: [
{
key: "AsanaAccountsKeywords",
input: { id: "KeywordsInput", label: "Keywords" },
},
],
},
footer: {
actions: [],
},
body: {
rowType: WIDGET_ITEM_TYPES.MATCHING_WIDGET,
},
};
return <WidgetTemplate widgetTemplate={widgetConfig} />;
}
Now, let’s say we need to change requirements. <WidgetHeader/>
can render a subtitle, tabs, or dropdown that can filter widget’s data. We also want to add a menu with custom actions and in some widgets we need to make rows editable.
We need to update the useWidgetTemplate
hook. I am not going to paste here the hook implementation, because this single function has around 35 arguments and more than 1k lines of code, it’s pretty huge.
If we want to implement a new feature or update current functionality, most probably we need to update the useWidgetTemplate
hook. Maybe it’s not so obvious, but can you sense what’s wrong with this code structure and how to fix this?
The dev team did a good decision to use hooks instead of HOC. However, the main issue is that we have a single function/hook with 1k+ lines of code and 35 arguments. In this case, devs tried to create some kind of “own framework” and they ended up with a huge monolithic function that is hard to maintain.
Now, we know this is wrong. How can we fix this?
When I was thinking about this use case, the first thing that came into my mind was react table library. If you want to use this library to build a table, you need to use useReactTable hook which argument is also a big configuration object. The main difference between our useWidgetTemplate
and useReactTable
function is that useReactTable
is not a big monolithic function. It works as an “orchestration function” that calls inner hooks/functions. You can see more details in the implementation of this hook. The useReactTable
arguments are logically grouped into inner objects.
Now, how can we fix our dashboard project?
- Each platform widget component
SalesforceWidget
,AsanaWidget
,Zendesk
etc. should be responsible for fetching and formatting it’s own data. This logic is unique for specific widget and don’t really belong to theuseWidgetTemplate
hook logic. - We can remove
WidgetFromTemplate
component and make composition of widget’s elements earlier in parent component. Check the code snippet here. - We can break down
useWidgetTemplate
function into smaller functions. The main functionuseWidgetTemplate
just orchestrates inner functions and arguments can be logically grouped into inner objects:
import { useRefetchData } from './useRefetchData';
import { useRows } from './useEditableRows';
import { useWidgetHeader } from './useWidgetHeader';
import { useWidgetFooter } from './useWidgetFooter';
import { useEditableRows } from './useEditableRows';
import { useWidgetDialog } from './useWidgetDialog';
import { useWidgetContent } from './useWidgetContent';
function useWidgetTemplate({
refetchArgs,
widgetRowsArgs,
widgetHeaderArgs,
widgetFooterArgs,
editableRowsArgs,
addFieldProps,
}) {
useRefetchData(...refetchArgs);
const rowsProps = useRows(...widgetRowsArgs);
const widgetHeaderProps = useWidgetHeader(...widgetHeaderArgs);
const widgetFooterProps = useWidgetFooter(...widgetFooterArgs);
const editableRowsProps = useEditableRows(...editableRowsArgs);
const dialogProps = useWidgetDialog();
const widgetContentProps = useWidgetContent(...addFieldProps);
return {
widgetHeaderProps,
widgetFooterProps,
rowsProps,
editableRowsProps,
widgetContentProps,
widgetProps: dialogProps,
};
}
In the new structure we can remove <WidgetFromTemplate />
component and we can make the widgets composition in the platform widget component. Implementation of a platform widget (AsanaWidget) would look like this:
import { restoreCachedOrFetchTasks } from './restoreCachedOrFetchTasks';
import { formatTasksToWidgetRows } from './formatTasksToWidgetRows'
// props: { id: 'ASANA_WIDGET' }
function AsanaWidget(props) {
useEffect(() => {
restoreCachedOrFetchTasks();
}, [restoreCachedOrFetchTasks]);
const formattedRowsData = formatTasksToWidgetRows({ tasks });
const {
widgetProps,
widgetHeaderParams,
widgetContentParams,
widgetFooterParams,
widgetModalParams,
widgetRowProps,
} = useWidgetTemplate({
props,
restoreCachedOrFetchTasks,
formattedRowsData
});
return (
<Widget widgetTemplate={widgetProps}>
<WidgetHeader {...widgetHeaderParams} title="Asana Data" />
<WidgetContent {...widgetContentParams}>
<WidgetRows rows={formattedRowsData} {...widgetRowProps} />
</WidgetContent>
<WidgetFooter {...widgetFooterParams} />
<WidgetModal {...widgetModalParams} />
</Widget>
);
}
As you can see, the new structure is much more flexible. If platform widget requires a bit different structure, we don’t need to update <WidgetFromTemplate />
. If we need to update widget funcionality, it’s much easier to update it in inner function of useWidgetTemplate
hook.
Form application
Now we have another project that is very form-centric. We have a lot of simple forms that have very similar layout. Developers decided to create a component
function GenericFormTest({ successFunction, style }) {
formTestFields = [
{
name: 'name',
label: 'Name',
value: '',
placeholder: 'What would you like to call this?',
type: 'textfield',
validators: ['required'],
},
{
name: 'description',
label: 'Description',
value: '',
placeholder: 'Tell me about it?',
type: 'textarea',
validators: ['required'],
},
{
name: 'count',
label: 'Count',
value: '',
placeholder: 'How many are there?',
type: 'textfield',
validators: ['required', 'isNumber'],
},
{
name: 'note',
label: 'Note',
value: '',
placeholder: '',
type: 'textarea',
textRows: 4,
},
{
name: 'Done',
label: 'Create',
cancelLabel: 'Cancel',
updateLabel: 'Update',
type: 'cancelsubmit',
cancelVariant: 'contained',
variant: 'contained',
color: 'primary',
},
];
render() {
return (
<GenericForm
style={style}
formElements={formTestFields}
successFunction={successFunction}
columns={1}
/>
);
}
}
Now, let’s say we have new requirements for our <GenericForm />
. For example, the form layout is one column, but some of its inputs have two columns layout. Then we have another requirement to add custom validations for some fields in javascript. <GenericForm />
becomes a component with 1k+ line of code. From the first look, you can see this implementation is completely wrong. Composition through props is not enough and we need more flexibility for form’s inner components. We can find inspiration in other form libraries and see that we need to make composition earlier in parent component to make our code structure more flexible.
Conclusion
In this case study we go through different use cases of composition in React projects. We have a lot of choices on how to break down React components. If we don’t do this right, it can cause a lot of issues later and cost us too much effort to fix it.
You can easily find this issue if you have a big monolithic function or component in your code base. If you don’t know how to fix it, try to find a parallel in successful open source React projects and check how they deal with composition.
I’d like to hear from you.
Do you have a similar experience with composition in React projects?