Replace Type Component with Subcomponents

Altrim Beqiri

Altrim Beqiri /

In a React project I'm currently developing, I encountered a component responsible for displaying an invoice, which is styled according to a theme specified in the settings. Within the InvoicePage component, we initially retrieve the invoice and theme from the Redux store. These are then forwarded as props to the InvoiceDetails component. The primary function of InvoiceDetails is to present the detailed information of the invoice, styled according to the passed properties. The component's structure is as follows:

enum Theme {
  Simple = 'Simple',
  Modern = 'Modern',
}

const InvoicePage: React.FC<{id: string}> = ({ id }) => {
  const invoice: Invoice =  useSelector((state) => state.invoices[id]);
  // returns one of "Simple" | "Modern";
  const theme: Theme = useSelector((state) => state.settings.theme);
  ...
  return (
    <>
      ...
      {/* We pass an invoice and a theme to the component */}
      <InvoiceDetails invoice={invoice} theme={theme} />
      ...
    </>
  );
};

Initially, I began with just the Simple theme, which meant there was no need for a property in the component to specify a theme type. Consequently, the component could be utilized as shown below:

<InvoiceDetails invoice={invoice} />

Upon incorporating the Modern theme, I introduced a new theme property to the component. At that stage, the differences between the themes were minimal, so adding a type (or theme) property was a logical step. Generally, this method proves quite effective for representing variations of a similar entity.

enum Theme {
  Simple = "Simple",
  Modern = "Modern",
}
<InvoiceDetails invoice={invoice} theme={Theme.Simple | Theme.Modern} />;

Following the introduction of the new theme, I made several updates to the InvoiceDetails component. These updates included adding variables and functions specific to the theme, along with conditional checks to determine which pieces of information should be displayed or hidden. After these enhancements, the component's structure appeared as follows:

const InvoiceDetails: React.FC<{invoice: Invoice, theme: Theme }> = ({ invoice, theme }) => {
  const isSimple = theme === Theme.Simple;
  const isModern = theme === Theme.Modern;

  const usedOnlyIfSimpleTheme = // ...
  const usedOnlyIfModernTheme = // ...

  const functionForASimpleTheme = () => {
      // Do Simple Things ...
  }

  const functionForAModernTheme = () => {
      // Do Modern Things ...
  }

  return (
    <>
      <header> Generic Information applicable to both themes </header>
      <section> Generic Information applicable to both themes </section>
      ...
      {isSimple ? <div> Display simple information </div> : null}
      ...
      {isModern ? <div> Display modern information </div> : null}
      ...
      <section> Generic Information applicable to both themes </section>
      ...
      // Additional similar checks ...
    </>
  );
};

Initially, managing just two theme options worked well. However, complications arose with the introduction of a third Classic theme. The component's code began to become cluttered, and the proliferation of functions and conditional statements became overwhelming with just three themes. This raised concerns about the potential complexity when adding a fourth or fifth theme.

At this stage, it became evident that the component needed refactoring to simplify the code. This would ensure that adding more themes in the future wouldn't result in an unwieldy tangle of conditionals and theme-specific functions.

For the refactoring I considered applying a method similar to the Replace Type Code with Subclasses technique. This approach involves removing the type attribute from the main class and creating specific subclasses for each type.

Typically explained using class-based examples, the essence of this refactoring can be summarized as follows:

// Refactor this 👇
function renderInvoice(invoice: Invoice, theme: Theme) {
  return new InvoiceDetails(invoice, theme);
}

// Into this 👇
function renderInvoice(invoice: Invoice, theme: Theme) {
  switch (theme) {
    case Theme.Simple:
      return new SimpleTheme(invoice);
    case Theme.Modern:
      return new ModernTheme(invoice);
    case Theme.Classic:
      return new ClassicTheme(invoice);
  }
}

In our scenario, we're working with React components. To apply a similar principle, we would create distinct components for each theme. These specialized components would then replace the single, type-based component. Essentially, our goal is to implement a structure like the one illustrated below:

// Refactor this 👇
<InvoiceDetails invoice={invoice} theme={Theme.Simple} />
<InvoiceDetails invoice={invoice} theme={Theme.Modern} />
<InvoiceDetails invoice={invoice} theme={Theme.Classic} />

// Into this 👇
const Details: Record<Theme, ReactNode> = {
  [Theme.Simple]: <SimpleTheme invoice={invoice} />,
  [Theme.Modern]: <ModernTheme invoice={invoice} />,
  [Theme.Classic]: <ClassicTheme invoice={invoice} />,
};

The mechanics to perform this refactoring are as follows

  • Keep the generic logic and layout applicable to all themes inside the InvoiceDetails as a wrapper component.
  • Create specific components for each theme: SimpleTheme, ModernTheme and ClassicTheme.
  • Move the specific variables, functions and layout to their respective components.
  • Wrap the subcomponents using the InvoiceDetails wrapper component.

Refactoring Steps

In the first step we create the InvoiceDetails wrapper component. We remove the type checks and only keep the generic variables, functions and layout.

const InvoiceDetails: React.FC = ({ children }) => {
  // Keep only generic variables and functions
  const genericForAllThemes = // ...
  const functionGenericForAllThemes = () => {
      // Do Generic Things ...
  }
  ...
  return (
    <>
      <header> Generic Information applicable to both themes </header>
      <section> Generic Information applicable to both themes </section>
      {children}
      <section> Generic Information applicable to both themes </section>
      <footer> Generic Information applicable to both themes </footer>
    </>
  );
};

In the second step we create specific components for each theme (type).

const SimpleTheme: React.FC<{ invoice: Invoice }> = ({ invoice }) => {};
const ModernTheme: React.FC<{ invoice: Invoice }> = ({ invoice }) => {};
const ClassicTheme: React.FC<{ invoice: Invoice }> = ({ invoice }) => {};

Lastly we move the specific variables, functions and layout to their respective components. And in the end we use the InvoiceDetails component as a wrapper.

const SimpleTheme: React.FC<{invoice: Invoice}> = ({ invoice }) => {
  const usedOnlyIfSimpleTheme = // ...
  const functionForASimpleTheme = () => {
      // Do Simple Things ...
  }
  ...
  return (
    <InvoiceDetails>
      ...
      <div>Simple Information</div>
      ...
    </InvoiceDetails>
  );
};
const ModernTheme: React.FC<{invoice: Invoice}> = ({ invoice }) => {
  const usedOnlyIfModernTheme = // ...
  const functionForAModernTheme = () => {
      // Do Modern Things ...
  }
  ...
  return (
    <InvoiceDetails>
      ...
      <div>Modern Information</div>
      ...
    </InvoiceDetails>
  );
};
const ClassicTheme: React.FC<{invoice: Invoice}> = ({ invoice }) => {
  const usedOnlyIfClassicTheme = // ...
  const functionForAClassicTheme = () => {
      // Do Classic Things ...
  }
  ...
  return (
    <InvoiceDetails>
      ...
      <div>Classic Information</div>
      ...
    </InvoiceDetails>
  );
};

With all of the refactoring in place our InvoicePage component looks like the following now

enum Theme {
  Simple = "Simple",
  Modern = "Modern",
  Classic = "Classic",
}

const InvoicePage: React.FC<{ id: string }> = ({ id }) => {
  const invoice: Invoice = useSelector((state) => state.invoices[id]);
  // returns one of "Simple" | "Modern" | "Classic";
  const theme: Theme = useSelector((state) => state.settings.theme);

  // We use a dictionary to map the enum to the component
  const Details: Record<Theme, ReactNode> = {
    [Theme.Simple]: <SimpleTheme invoice={invoice} />,
    [Theme.Modern]: <ModernTheme invoice={invoice} />,
    [Theme.Classic]: <ClassicTheme invoice={invoice} />,
  };

  return (
    <>
      ...
      {/* Render the specific Theme component based on the settings */}
      {Details[theme]}
      ...
    </>
  );
};

When the time comes to add another theme in the future we specify the type in the Theme enum, create a specific component for that theme and add it to the dictionary.

And that's it for this one. I hope this technique will be helpful to you as well.

Do you use similar techniques in your code? Tweet me @altrimbeqiri and let me know.

Happy Coding!