Replace Type Component with Subcomponents

Altrim Beqiri

Altrim Beqiri /

In a React application that I am working on I came across this component that I am using to render an invoice based on a theme provided in settings. In the InvoicePage component we first get the invoice and theme from the redux store and then pass them as properties to the InvoiceDetails component. Then the InvoiceDetails component's responsibility is to render the invoice information details based on the provided properties. The component looks something like this

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} />
      ...
    </>
  );
};

At first I started with only the initial Simple theme, so there was no property in the component to pass a theme (type), you could use it like the following

<InvoiceDetails invoice={invoice} />

As I added the second Modern theme I introduced the new theme property. At that point there were not many differences between the themes so introducing a type (theme) property made sense. And honestly most of the time this approach works perfectly fine when you want to represent different kinds of a similar thing.

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

With that addition, inside the InvoiceDetails component based on the theme I started adding few variables and functions, as well as conditional checks in order to display or hide various information throughout the component.

With those additions the component looked similar to the following

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 ...
    </>
  );
};

This was ok at first with only two options. The issues started appearing when I needed to add a third Classic theme. The component code started getting convoluted, the amount of functions and conditionals was getting out of hand with only three themes. What would happen when the time comes to add the fourth or fifth theme?

At this point it was time to refactor the component and clean up the code so that even if we add additional themes in future we won't have to deal with all the conditionals and specific function behaviors depending on the theme type.

For the refactoring I thought to use a similar technique to Replace Type Code with Subclasses. The idea with this technique is to remove the type from the main class and replace it with specific type sub classes.

The refactoring technique is usually explained by using classes, and in a gist the refactoring looks something like this

// 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);
  }
}

Though in our case we are dealing with React components. So, to make this work we would instead create specific components for each type and use the subcomponents to replace the type component. Essentially what we want is to achieve the following

// 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 at @altrimbeqiri and let me know.

Happy Coding!