DEV Community

Ricardo Silva
Ricardo Silva

Posted on • Updated on

The Future of React: Enhancing Components through Composition Pattern

Introduction

Hello Fellow Developers! 👋

I can hardly contain my enthusiasm as I've been exploring into a revolutionary approach for structuring my components - the Composition Pattern in React! This strategy is a game-changer, elevating reusability, readability, and maintainability of the code to new heights. It provides a stunning advantage by enabling the addition or modification of functionality without ramping up complexity.

As a programmer, my mission goes beyond writing code. It's about creating software that simplifies people's lives, all while ensuring that this software remains scalable and a breeze to work with. I'm confident that this methodology can massively contribute to achieving this mission.

Fasten your seatbelts folks and get ready, we're blasting off into the future of code! 🚀

The Traditional Approach

In "traditional component architecture", it is common to encounter the need for passing numerous props throughout the component tree to incorporate custom styles or behaviours.
When starting a new project, it seems straightforward to introduce a new prop to handle a new specific behaviour or style. However, through my coding experience, I have come to realise this can easily lead to a pitfall. This approach eventually presents challenges in tracking which props impact which components at the same time it adds lots of complexity to maintenance due to the introduction or modification of functionality over time.

Let's take a typical example of a Call to Action (CTA) button. I chose to adopt this component because I believe that many developers have been caught up in the past, underestimating its value due to the perception of it being a simple button with text and styles. Therefore, I consider it to be one of the most under appreciated components.😂

A design of a Call to Action (CTA)
Let's build the CTA component based on the given design, which includes a text and an optional icon positioned on the right side.

interface CtaButtonProps extends React.ComponentProps<'button'> {
  variant?: 'primary' | 'secondary';
  size?: 'small' | 'large';
  iconName?: string;
  text: string;
}

export const CtaButton: React.FC<CtaButtonProps> = ({
  variant = 'primary',
  size = 'medium',
  iconName,
  text,
  className,
  ...props
}) => {
  const classes = `btn ${variant} ${size} ${className}`;

  return (
    <button className={classes} {...props}>
      <span>{text}</span>
      {iconName && <Icon name={iconName} size="10" />}
    </button>
  );
};

Enter fullscreen mode Exit fullscreen mode

And here's how it can be utilized:

<CtaButton iconName="+" text="Add more" />
Enter fullscreen mode Exit fullscreen mode

I agree, this CTA appear straightforward, but I've learned never to put my complete faith in the designer. 🤣

Following a week, the requirements for the CTA changed. Now, it needs to be capable of displaying an icon on both the left and right sides, with the option to show them together or independently. Furthermore, the colour and size of each icon should be customisable based on where the CTA will be placed within the application.

Let's implement these modifications and observe the result:

interface CtaButtonProps extends React.ComponentProps<'button'> {
  variant?: 'primary' | 'secondary';
  size?: 'small' | 'large';
  iconNameLeft?: string;
  iconNameRight?: string;
  iconColorLeft?: string;
  iconColorRight?: string;
  iconSizeLeft?: number;
  iconSizeRight?: number;
  text: string;
}

export const CtaButton: React.FC<CtaButtonProps> = ({
  variant = 'primary',
  size = 'medium',
  iconNameLeft,
  iconNameRight,
  iconColorLeft,
  iconColorRight,
  iconSizeLeft,
  iconSizeRight,
  text,
  className,
  ...props
}) => {
  const classes = `btn ${variant} ${size} ${className}`;

  return (
    <button className={classes} {...props}>
      {iconNameLeft && (
        <Icon name={iconNameLeft} size={iconSizeLeft} color={iconColorLeft} />
      )}
      <span>{text}</span>
      {iconNameRight && (
        <Icon
          name={iconNameRight}
          size={iconSizeRight}
          color={iconColorRight}
        />
      )}
    </button>
  );
};
Enter fullscreen mode Exit fullscreen mode

How it would be used:

<CtaButton
   iconNameLeft="+"
   iconColorLeft="red"
   iconNameRight="+"
   iconColorRight="green"
   iconSizeLeft={20}
   iconSizeRight={30}
   text="Add more"
/>
Enter fullscreen mode Exit fullscreen mode

And here we have the outcome.
We went from having 4 props to now having 9 props. Although it is relatively simple to comprehend which prop affects what in this case, it's important to remember that this is merely a basic example illustrating how minor changes in requirements can easily derail things.

The Composition Pattern

The Composition Pattern comes to the rescue to simplify these concerns.

Transition to Composition

Now, let's transform the previous example using the Composition Pattern:

interface CtaRootProps extends React.ComponentProps<'button'> {
  variant?: 'primary' | 'secondary';
  size?: 'small' | 'large';
}

export const CtaRoot: React.FC<CtaRootProps> = ({
  variant = 'primary',
  size = 'medium',
  className,
  children,
  ...props
}) => {
  const classes = `btn ${variant} ${size} ${className}`;

  return (
    <button className={classes} {...props}>
      {children}
    </button>
  );
};

export const CtaIcon: React.FC<React.ComponentProps<typeof Icon>> = (props) => {
  return <Icon {...props} />;
};

export const CtaText: React.FC<React.ComponentProps<'span'>> = ({
  children,
  ...props
}) => {
  return <span {...props}>{children}</span>;
};

export const Cta = {
  Root: CtaRoot,
  Icon: CtaIcon,
  Text: CtaText,
};
Enter fullscreen mode Exit fullscreen mode

Understanding the Transition

In the above example, we've taken our single, prop-heavy CtaButton component and broken it down into three smaller, more manageable components: CtaRoot, CtaIcon, and CtaText. This is the core principle of the Composition Pattern - breaking components down into smaller, reusable parts that can be composed together to form more complex UIs.

Usage of Composition

Looking to the previous change of requirements to the Cta, with the Composition Pattern, we can effortlessly configure a CTA with an icon on the left, right, or none at all, not having to add any other logical complexity or a prop hell, instead we compose elements to achieve our desired UI.
Each icon can now be unique, with a different size, color, and more, because each element is exposed. 🤩

{/* Icon Left */}
<Cta.Root>
  <Cta.Icon name="+" color="red" size={20} />
  <Cta.Text>Add more</Cta.Text>
</Cta.Root>

{/* Icon Right */}
<Cta.Root>
  <Cta.Text>Add more</Cta.Text>
  <Cta.Icon name="+" color="green" size={20} />
</Cta.Root>

{/* Icon Both sides with different colors and sizes */}
<Cta.Root>
  <Cta.Icon name="+" color="red" size={20} />
  <Cta.Text>Add more</Cta.Text>
  <Cta.Icon name="+" color="green" size={30} />
</Cta.Root>
Enter fullscreen mode Exit fullscreen mode

Advantages of Composition Pattern

Embracing the Composition Pattern comes with several key benefits:

  1. Reduced Prop-Drilling
    Exporting components individually allows each sub-component to maintain its own props, eliminating the need to pass props down multiple levels.

  2. Explicit Control
    Each sub-component can be included or excluded explicitly in the parent component. This eliminates the need for internal conditional logic to apply the different styles or behaviours.

  3. Easier Variants
    If new design requirements emerge, such as having an icon on the left instead of the right, we can easily rearrange our composed sub-components without the need for additional props or logic.

  4. Clear Structure
    The composition approach provides a clear structure of the expected child components and their relationship to the parent, making it easier to understand what a component's structure is at a glance.

Conclusion

Incorporating the Composition Pattern can significantly assist in maintaining a clean, modifiable, and expandable codebase. I view this as a considerable advancement in the strategy of crafting components and felt compelled to share this with you all.

I'm excited to hear your insights on this!
Let's revolutionise the way we craft React components together.

Top comments (3)

Collapse
 
chrispepper1989 profile image
Christopher John Pepper

This is brilliant and reminds of the shift in games programming (and general OOO) from using inheritance to composition.

I can't believe I didn't spot this repeating pattern before! Great article

Collapse
 
ricardolmsilva profile image
Ricardo Silva

Truly appreciate your comment @chrispepper1989, definitely motivates me to keep documenting my leanings.

Glad that you find it useful!

Meanwhile I extended the article, I understood that some people less fluent in typescript was struggling to understand the advantages, thinking that implement the composition pattern, the typescript hell shown in the example was required.

So now it gives a bit more of context showing the before and after and reduces the complexity of typescript.

Hope this did not get too extended now 😂

Collapse
 
codajoao profile image
João Paulo

This really makes sense. Thanks for share :)