Skip to content

SOLID in React - Open-Closed Principle

Posted on:February 12, 2024 at 07:27 PM (5 min read)

Best practices for writing clean code in React - Open-Closed Principle

What is Open-Closed Principle?

The Open-Closed Principle (OCP) is a fundamental concept in software engineering, particularly in the context of object-oriented programming. When applying OCP to React components, the goal is to ensure that components are open for extension but closed for modification.

Why is OCP important?

  1. Maintainability: Components are easier to maintain and evolve over time because new features can be added without modifying existing code.
  2. Scalability: Systems designed with the Open/Closed Principle in mind are more scalable, allowing for the addition of new functionality without introducing regressions.
  3. Flexibility: Developers have the flexibility to extend and customize components to suit changing requirements without impacting existing functionality.

How to apply OCP in React?

Let’s consider a Button component as an example to illustrate the Open/Closed Principle. The Button component is a common UI element used throughout applications, and it often requires customization to suit different contexts.

Bad example

interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  text: string;
  icon?: "arrow" | "arrowLeft" | "arrowRight" | "plus";
}

export function Button({ text, role, icon }: Props) {
  return (
    <button {...props}>
      {text}
      <div>
        {icon === "arrow" && <ArrowIcon />}
        {icon === "arrowLeft" && <ArrowLeftIcon />}
        {icon === "arrowRight" && <ArrowRightIcon />}
        {icon === "plus" && <PlusIcon />}
      </div>
    </button>
  );
}

In a bad implementation of the Button component, we encounter a scenario where the component’s functionality is tightly coupled with specific icon representations. Let’s delve into the issues with this approach:

  1. Tight Coupling: The Button component tightly couples its functionality with specific icon representations (ArrowIcon, ArrowLeftIcon, ArrowRightIcon, PlusIcon). This means that any changes or additions to the available icons require modifications directly within the Button component itself. This violates the Open/Closed Principle, which suggests that software entities should be open for extension but closed for modification.

  2. Lack of Flexibility: By directly embedding icon components within the Button component based on specific icon names (icon === ‘arrow’, icon === ‘arrowLeft’, etc.), it becomes challenging for users to extend or customize the button’s icon functionality without altering the component’s source code. This limits the flexibility of the Button component and hampers its ability to adapt to different icon requirements in diverse contexts.

  3. Limited Reusability: The tightly coupled nature of the Button component makes it less reusable across different projects or scenarios. Any modifications made to accommodate new icons or icon variations in one project may not be easily transferable to other projects, leading to code duplication and maintenance overhead.

  4. Potential Code Bloat: As more icon options are introduced, the Button component’s codebase may grow significantly, becoming harder to maintain and comprehend. This can lead to code bloat, decreased readability, and increased chances of introducing bugs when making changes.

Good example

interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  text: string;
  icon?: React.ReactNode;
}

export function Button({ text, role, icon }: Props) {
  return (
    <button {...props}>
      {text}
      <div>
       {icon}
      </div>
    </button>
  );
}

// Usage
<Button text="Submit" icon={<ArrowIcon />} />
<Button text="Back" icon={<ArrowLeftIcon />} />
<Button text="Next" icon={<ArrowRightIcon />} />
<Button text="Add" icon={<PlusIcon />} />

In a good implementation of the Button component, we’ve addressed the issues present in the previous example by decoupling the button’s functionality from specific icon representations. Let’s explore the improvements:

  1. Decoupled Functionality: The Button component no longer relies on hardcoded icon names to determine which icon to render. Instead, it accepts an icon prop of type React.ReactNode, allowing users to pass any JSX element as the button’s icon. This decouples the button’s functionality from specific icon representations, promoting flexibility and extensibility.

  2. Enhanced Flexibility: By accepting a React.ReactNode for the icon prop, the Button component becomes more flexible and versatile. Users can easily customize the button’s icon by passing any JSX element, including built-in icon components (ArrowIcon, ArrowLeftIcon, etc.) or custom icon implementations. This empowers users to tailor the button’s appearance to their specific needs without modifying the Button component’s source code.

  3. Improved Reusability: The decoupled nature of the Button component enhances its reusability across different projects and scenarios. Users can leverage the Button component with various icon configurations without having to make modifications to the component itself. This promotes code reuse, reduces redundancy, and simplifies maintenance efforts across projects.

  4. Streamlined Codebase: With the removal of hardcoded icon representations, the Button component’s codebase becomes more concise and maintainable. The component’s logic is focused solely on rendering the button’s text and icon, leading to cleaner, more readable code. This streamlines development workflows and reduces the risk of introducing bugs when making changes.

In conclusion, the improved implementation of the Button component demonstrates adherence to the Open/Closed Principle by decoupling functionality, promoting flexibility, enhancing reusability, and streamlining the component’s codebase. By embracing these principles, we create software components that are resilient to change, adaptable to evolving requirements, and conducive to long-term maintainability.

Summary

In React, adhering to the Open-Closed Principle (OCP) is crucial. It keeps components open for extension while closed for direct changes. The example with the Button component illustrates this well. In the bad implementation, tight links between functionality and specific icons led to issues. However, the good example, with a more flexible approach, showcases improved adaptability and maintainability. Embracing OCP ensures cleaner, scalable React components that can easily grow with evolving needs.