![shadow](/_next/image?url=%2Fimages%2Fblog%2Fshadow.png&w=3840&q=75)
![Open/Closed Principle: Writing Scalable Code in React](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1737866378725%2F2d071961-0eba-43d8-b577-e9cd188e0f7a.png&w=3840&q=75)
Open/Closed Principle: Writing Scalable Code in React
Build scalable and maintainable web applications.
Welcome back to our journey through the SOLID principles! In our last blog, we talked about the Single Responsibility Principle (SRP) and how it helps us write clean, maintainable code. If you missed it, go check it out first—it’s the foundation for everything else we’re discussing in this series.
Today, we’re diving into the O - Open/Closed Principle (OCP), an essential pillar of software design that ensures your code is scalable and flexible while minimizing regressions.
If you’ve ever worked on a React project and found yourself modifying existing components over and over again to add new features, you’re probably violating this principle. But don’t worry—I’ll walk you through practical, real-world examples that make this concept easy to understand and apply.
By the end of this post, you’ll not only grasp the essence of OCP but also be able to write better, more extensible React components. Ready? Let’s get started!
What is the Open/Closed Principle (OCP)?
The Open/Closed Principle, coined by Bertrand Meyer, is defined as:
A software entity (class, module, function, etc.) should be open for extension but closed for modification.
In simpler terms:
• Open for extension: You should be able to add new functionality to your code without changing its existing implementation.
• Closed for modification: Existing code should remain untouched to avoid introducing bugs or breaking existing features.
The principle encourages developers to build modular and flexible systems by abstracting and encapsulating logic, reducing the risk of regressions when requirements evolve.
Why is OCP Important?
Failing to adhere to OCP often results in tightly coupled code that becomes increasingly hard to maintain as your application grows. Some of the key benefits of applying OCP include:
1. Scalability: Adding new features without disrupting existing code makes your application more adaptable to change.
2. Stability: Existing functionality remains untouched, reducing the risk of introducing bugs.
3. Testability: Isolated extensions are easier to test.
4. Collaboration: Different developers can work on extensions without stepping on each other’s toes.
Now that we understand the importance of OCP, let’s see how to implement it in React.
OCP in React: Practical Examples
In React, OCP is all about designing extensible components, hooks, and modules without modifying their core logic. Let’s examine some scenarios in which this principle can be applied.
1. Extending UI Components with Props and Composition
One of React’s core strengths is composition, which aligns perfectly with OCP. Instead of hardcoding logic into components, use props and composition to make them extensible.
Problem: A button component that needs different styles and behaviors for various use cases.
// Not OCP-friendly: Hardcoding styles and logic
const Button = ({ type }: { type: string }) => {
if (type === 'primary') {
return <button className="bg-blue-500 text-white">Primary Button</button>;
}
if (type === 'secondary') {
return <button className="bg-gray-500 text-black">Secondary Button</button>;
}
return <button>Default Button</button>;
};
This implementation violates OCP because adding new button types requires modifying the existing component.
Solution: Use props and composition to make the component extensible.
// OCP-friendly: Open for extension via props
interface ButtonProps {
children: React.ReactNode;
className?: string;
onClick?: () => void;
}
const Button: React.FC<ButtonProps> = ({ children, className, onClick }) => {
return (
<button className={`px-4 py-2 rounded ${className}`} onClick={onClick}>
{children}
</button>
);
};
// Usage examples
<Button className="bg-blue-500 text-white">Primary Button</Button>;
<Button className="bg-gray-500 text-black">Secondary Button</Button>;
<Button className="bg-green-500 text-white">Success Button</Button>;
Here, the Button component is closed for modification but is open for extension by passing new styles or children as props.
2. Implementing Strategy Pattern with Custom Hooks
React hooks provide a powerful way to abstract logic and make it reusable. By applying the strategy pattern, you can extend functionality without modifying the core hook.
Problem: A hook that fetches data from an API but requires different URLs for various endpoints.
// Not OCP-friendly: Hardcoding the endpoint
const useFetchData = () => {
const [data, setData] = React.useState(null);
useEffect(() => {
fetch('/api/users')
.then((res) => res.json())
.then((data) => setData(data));
}, []);
return data;
};
This violates OCP because the endpoint is hardcoded, making it impossible to reuse the hook for different APIs.
Solution: Accept the endpoint as a parameter to extend the hook’s functionality.
// OCP-friendly: Open for extension via parameters
const useFetchData = (endpoint: string) => {
const [data, setData] = React.useState(null);
useEffect(() => {
fetch(endpoint)
.then((res) => res.json())
.then((data) => setData(data));
}, [endpoint]);
return data;
};
// Usage examples
const users = useFetchData('/api/users');
const posts = useFetchData('/api/posts');
Here, the hook is extensible to fetch data from any endpoint without modifying its internal implementation.
3. Extending Configurations with Factory Functions
Another practical example of OCP is using factory functions to dynamically create configurations or components.
Problem: You have a chart component that needs different configurations for bar charts, pie charts, and line charts.
// Not OCP-friendly: Hardcoding configurations
const Chart = ({ type }: { type: string }) => {
if (type === 'bar') {
return <BarChart data={data} />;
}
if (type === 'pie') {
return <PieChart data={data} />;
}
return null;
};
Solution: Use a factory function to generate configurations dynamically.
// OCP-friendly: Open for extension with factory functions
const createChartConfig = (type: string) => {
switch (type) {
case 'bar':
return { type: 'bar', options: { ... } };
case 'pie':
return { type: 'pie', options: { ... } };
default:
throw new Error('Unsupported chart type');
}
};
const Chart = ({ type }: { type: string }) => {
const config = createChartConfig(type);
return <ChartComponent config={config} />;
};
This approach allows you to add new chart types without altering the existing logic.
Common Mistakes When Applying OCP
1. Over-Engineering: Adding abstractions where they’re not needed can complicate the code unnecessarily.
2. Tight Coupling: Relying on concrete implementations instead of abstractions.
3. Violating SRP: Trying to handle too many extensions in a single module. Always adhere to SRP when implementing OCP.
Best Practices for OCP in React
1. Use Composition: Favor composition over inheritance to extend components.
2. Abstract Logic: Use hooks and utility functions to encapsulate reusable logic.
3. Leverage TypeScript: Use interfaces and generics to define extensible types.
4. Test Extensions: Ensure your extensions don’t inadvertently break existing functionality.
Conclusion: Embrace Extensibility Without Breaking Stability
The Open/Closed Principle is a powerful design guideline that helps you build flexible, maintainable, and scalable React applications. By designing components, hooks, and modules that are open for extension but closed for modification, you reduce the risk of breaking existing functionality while keeping your codebase adaptable to future requirements.
This blog is part of our series on the SOLID principles. In the next instalment, we’ll tackle the Liskov Substitution Principle (L) and explore how to write substitutable components for better reusability and consistency.
Stay tuned, and happy coding! 🚀
We at CreoWis believe in sharing knowledge publicly to help the developer community grow. Let’s collaborate, ideate, and craft passion to deliver awe-inspiring product experiences to the world.
Let's connect:
This article is crafted by Chhakuli Zingare, a passionate developer at CreoWis. You can reach out to her on X/Twitter, LinkedIn, and follow her work on the GitHub.