Open-Closed Principle (OCP) In React: Write Extensible Components
A practical look at applying the Open-Closed Principle to React. (4 minutes)
I shared an article some time ago about the SOLID Principles and how applying them can help us write more robust and maintainable code.
Theoretically, if you ask less experienced engineers about SOLID, everything is “okay”.
However, when it comes to applying SOLID, things start to get a bit harder.
In today’s article, I’d like to provide a more practical approach to applying the Open-Closed Principle (OCP) to React.
If you missed the post on applying the Single Responsibility Principle into React, check it out here:
What is the Open-Closed Principle (OCP)?
By definition, the principle states:
“A software artifact should be open for extension but closed for modification”.
In other words, you should strive to write your code in a way that when you add a new functionality, it shouldn’t require changing the existing code.
You can read more about the Open-Closed Principle (OCP) in my previous article.
Violating the Open-Closed Principle (OCP) in React
Let’s first see how things look like when we violate the OCP in React and write not so good enough components.
Consider adding variants to a <Button />.
const Button = ({ variant, ...restProps }) => {
let classes = "border border-transparent rounded-md";
if (variant === "primary") {
classes += " text-white bg-primary-500 ring-primary-300";
} else if (variant === "secondary") {
classes += " text-white bg-secondary-500 ring-secondary-300";
}
return <button className={classes} {...restProps} />;
};
The problem with this component is that:
adding new variants requires changing the component’s internal logic
the component needs to know internally about all possible variants
each new variant makes the component harder to read, test, and maintain
Be the Senior who delivers the standard for writing robust React Components.
Write Better Components by Building Open/Extensible Components
A better alternative is to build open components which could be reused and extended when requirements change, instead of changing their internal implementation.
Here is the refactored example:
type VariantPrefix<T extends string, C extends string> = `${T}-${C}`;
type Colors =
| "primary"
| "secondary"
| "success"
| "danger"
| "warning"
| "info";
type ButtonVariants =
| Colors
| VariantPrefix<"outline", Colors>
| VariantPrefix<"link", Colors>;
const Button = ({ variant, ...restProps }) => {
const {
theme: { button: theme },
} = useTheme();
const classes = theme.variant[variant];
return <button className={classes} {...restProps} />;
};
Now:
Variants styles are managed externally, through a theme, removing the need of the <Button /> component to know internally about the possible variants.
New variants are added inside the theme configuration without modifying the <Button /> component.
The <Button /> component is simplified which makes it easier to test, read, and maintain.
Theme is a configuration object. It looks something like:
const button: ButtonThemeProps = {
// ...
variant: {
primary: `
text-white bg-primary-500 ring-primary-300
`,
secondary: `
text-white bg-secondary-500 ring-secondary-300
`,
},
// ...
};
The theme configuration object is shared through the built-in React Context API and a custom hook.
Testing Benefits of Following OCP in React
Looking at the example above, now testing the <Button /> component is much easier and straightforward.
const defaultProps = {
children: "Content",
};
const renderComponent = (props = {}) =>
render(<Button {...defaultProps} {...props} />);
describe("Button", () => {
describe("Props", () => {
it("should match snapshot with default (solid) `variant`", () => {
const props = {
variant: "primary",
};
const { container } = renderComponent(props);
expect(container).toMatchSnapshot();
});
it("should match snapshot with outline `variant`", () => {
const props = {
variant: "outline-primary",
};
const { container } = renderComponent(props);
expect(container).toMatchSnapshot();
});
});
});
Custom Hooks and OCP in React
We can even follow the OCP when building custom hooks in React.
We can have one custom hook for doing the fetching and calling the API, and another custom hook for fetching the user data
For example:
const useFetch = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((data) => {
setData(data);
setLoading(false);
});
}, [url]);
return { data, loading };
};
const useUserData = (userId) => {
const url = `/api/users/${userId}`;
return useFetch(url);
};
const UserProfile = ({ userId }) => {
const { data, loading } = useUserData(userId);
if (loading) return <div>Loading...</div>;
return <div>{data.name}</div>;
};
📋 Recap
Prefer composition over modification - extend through props.
Create base components - make components easily to extend and compose.
Prefer custom hooks - enable reusable and maintainable extensions.
Prefer TypeScript - ensure props and extensions are type-safe.
Think about possible extensions points - what might change in the future?
That's all for today. I hope this was helpful. ✌️
How I can help you further?
Become the Senior React Engineer at your company! 🚀
👋 Let’s connect
You can find me on LinkedIn, Twitter(X), Bluesky, or Threads.
I share daily practical tips to level up your skills and become a better engineer.
Thank you for being a great supporter, reader, and for your help in growing to 22.1K+ subscribers this week 🙏
You can also hit the like ❤️ button at the bottom to help support me or share this with a friend. It helps me a lot! 🙏
Cool explanation Petar!