Wrong Component Composition in React.js Projects [Case Study on Why Software Projects Fail]

React.js

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.

reactjs dashboard project

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 />.

reactjs widget composition

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.

reactjs widget header composition

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 the useWidgetTemplate 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 function useWidgetTemplate 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 and pass its all elements by using a configuration object. Its usage looks like this:


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?

NEED A REACT EXPERT? LET’S BUILD SOMETHING.

GET IN TOUCH

NEED A FULL STACK WEB DEVELOPER? LET'S BUILD SOMETHING.

GET IN TOUCH

Leave a Reply

Your email address will not be published. Required fields are marked *

%d bloggers like this: