The “O” in SOLID stands for the Open/Closed Principle, which says:
“Software entities should be open for extension, but closed for modification.”
In other words, you shouldn't change existing code to add a new feature—you should extend it instead.
Let’s abstract a base component and allow new types to be created without modifying the original.
interface CustomButtonProps {
label?: string
}
export interface CustomButton {
render: (props: CustomButtonProps) => JSX.Element
}
With this setup, you can create separate implementations.
This is different from having a type: "primary" | "secondary"
inside a single button and managing logic internally.
// button.ts import { CustomButton } from './button-types' export const Button: CustomButton = {
render: ({ label }) => (
<button type="button" style={{ backgroundColor: 'blue', color: 'white' }}>
{label}
</button>
),
}
export const CancelButton: CustomButton = {
render: ({ label }) => (
<button type="button" style={{ border: '1px solid red', color: 'red' }}>
{label}
</button>
),
}
Now, the render
function allows you to create custom buttons however you like.
Its only responsibility is to pass the props and return JSX.
With the abstraction in place, now it’s time to implement a component just for rendering, like this:
// button-renderer.tsx import { CustomButton } from './button-types' type ButtonRendererProps = {
component: CustomButton label: string
}
export function ButtonRenderer({ component, label }: ButtonRendererProps) {
return component.render({ label })
}
With this, you never need to touch this component again.
You only modify or add new button implementations.
This is the core of OCP:
“Open for extension, closed for modification.”
That’s exactly what we’ve done here:ButtonRenderer
is closed for changes, but open for extension—just create a new CustomButton
.
Easier to add new features – Need a new alert button? Just create a file, no need to touch ButtonRenderer
.
Better maintainability – If your code has a lot of if
or switch
statements, it becomes rigid and harder to update.
Single responsibility – Each component knows only how to render itself.
Scalability – In large projects, you can build a library of button types that evolve independently.
More predictable tests – You can test each button in isolation.