Frontend Development 7 min read

Making React Components Open‑Closed: Extensible Patterns and Refactoring

This article explains how the Open‑Closed Principle applies to React development, demonstrating anti‑patterns and refactoring techniques—including base components, composition, higher‑order components, and custom hooks—to create extensible, maintainable UI elements while keeping tests simple.

KooFE Frontend Team
KooFE Frontend Team
KooFE Frontend Team
Making React Components Open‑Closed: Extensible Patterns and Refactoring

The Open‑Closed Principle (OCP) states that software entities should be open for extension but closed for modification; in React this means building components that can be extended without changing existing code.

Problems with Closed Components

A common anti‑pattern is hard‑coding variant handling inside a component, which forces developers to modify the component each time a new variant is added.

<code>// Don't do this
const Button = ({ label, onClick, variant }: ButtonProps) => {
  let className = "button";

  // Directly modify for each variant
  if (variant === "primary") {
    className += " button-primary";
  } else if (variant === "secondary") {
    className += " button-secondary";
  } else if (variant === "danger") {
    className += " button-danger";
  }

  return (
    <button className={className} onClick={onClick}>
      {label}
    </button>
  );
};
</code>

Building Open Components

Refactor by extracting a base component and creating thin variant wrappers, so new variants can be added without touching the original code.

<code>type ButtonBaseProps = {
  label: string,
  onClick: () => void,
  className?: string,
  children?: React.ReactNode,
};

const ButtonBase = ({ label, onClick, className = "", children }: ButtonBaseProps) => (
  <button className={`button ${className}`.trim()} onClick={onClick}>
    {children || label}
  </button>
);

// Variant components
const PrimaryButton = (props: ButtonBaseProps) => (
  <ButtonBase {...props} className="button-primary" />
);
const SecondaryButton = (props: ButtonBaseProps) => (
  <ButtonBase {...props} className="button-secondary" />
);
const DangerButton = (props: ButtonBaseProps) => (
  <ButtonBase {...props} className="button-danger" />
);

// Add new variant without modifying original component
const OutlineButton = (props: ButtonBaseProps) => (
  <ButtonBase {...props} className="button-outline" />
);
</code>

Component Composition Pattern

Using composition and render props enables flexible UI structures while keeping the core component closed.

<code>type CardProps = {
  title: string,
  children: React.ReactNode,
  renderHeader?: (title: string) => React.ReactNode,
  renderFooter?: () => React.ReactNode,
  className?: string,
};

const Card = ({ title, children, renderHeader, renderFooter, className = "" }: CardProps) => (
  <div className={`card ${className}`.trim()}>
    {renderHeader ? renderHeader(title) : <div className="card-header">{title}</div>}
    <div className="card-content">{children}</div>
    {renderFooter && renderFooter()}
  </div>
);

const ProductCard = ({ product, onAddToCart, ...props }: ProductCardProps) => (
  <Card {...props} renderFooter={() => (
    <button onClick={onAddToCart}>Add to Cart - ${product.price}</button>
  )} />
);
</code>

Higher‑Order Component Extension

A higher‑order component (HOC) can add cross‑cutting concerns such as loading states without altering the wrapped component.

<code>type WithLoadingProps = { isLoading?: boolean };

const withLoading = <P extends object>(WrappedComponent: React.ComponentType<P>) => {
  return ({ isLoading, ...props }: P & WithLoadingProps) => {
    if (isLoading) {
      return <div className="loader">Loading...</div>;
    }
    return <WrappedComponent {...props as P} />;
  };
};

// usage
const UserProfileWithLoading = withLoading(UserProfile);
</code>

Custom Hook Following OCP

Encapsulating data fetching in a hook makes it reusable and extensible; additional hooks can compose it without modifying the original implementation.

<code>const useDataFetching = <T,>(url: string) => {
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchData();
  }, [url]);

  const fetchData = async () => {
    try {
      const response = await fetch(url);
      const result = await response.json();
      setData(result);
    } catch (e) {
      setError(e as Error);
    } finally {
      setLoading(false);
    }
  };

  return { data, error, loading, refetch: fetchData };
};

const useUserData = (userId: string) => {
  const result = useDataFetching<User>(`/api/users/${userId}`);

  const updateUser = async (data: Partial<User>) => {
    // update logic
  };

  return { ...result, updateUser };
};
</code>

Testing Advantages

Because components are closed for modification, tests can focus on each variant independently, simplifying the test suite.

<code>describe("ButtonBase", () => {
  it("renders with custom className", () => {
    render(<ButtonBase label="Test" onClick={() => {}} className="custom" />);
    expect(screen.getByRole("button")).toHaveClass("button custom");
  });
});

// New variant tests
describe("PrimaryButton", () => {
  it("includes primary styling", () => {
    render(<PrimaryButton label="Test" onClick={() => {}} />);
    expect(screen.getByRole("button")).toHaveClass("button button-primary");
  });
});
</code>

Key Takeaways

Prefer composition over modification—extend via props and render props.

Create a stable base component that remains closed for changes.

Leverage higher‑order components and custom hooks for reusable extensions.

Identify extension points early to anticipate future variations.

Use TypeScript to enforce type‑safe extensions.

Conclusion

Applying the Open‑Closed Principle in React leads to components that are easier to maintain, test, and extend; combined with SOLID principles, it supports building robust, scalable front‑end architectures.

TypeScriptReactcomponent designHigher-Order ComponentsCustom HooksCompositionOpen-Closed Principle
KooFE Frontend Team
Written by

KooFE Frontend Team

Follow the latest frontend updates

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.