SOLID principles are a set of clean code conventions aimed towards writing maintainable code. These are originally object-oriented programming best practices. However, they can be adapted to JavaScript frameworks as well.
Single responsibility principle
A module needs to have only one responsibility. A module can be a component, function, class or interface.
E.g. Buttons
is responsible for rendering a list of buttons and Button
is responsible for rendering a single button.
const App = () => {
const Button = ({ children }) => (
<button>{children}</button>
);
const Buttons = ({ children }) => (
<div>{children}</div>
);
return (
<Buttons>
<Button>Task 1</Button>
<Button>Task 2</Button>
<Button>Task 3</Button>
</Buttons>
);
};
Open-closed principle
Components should be open to extension but closed to modification. If a new feature needs to be added to a component, we should not modify the component. Instead, we need to create a new component that extends the existing component.
E.g. TooltipButton
uses Button
internally but we never modify Button
's code. We simply extend its functionality.
const TooltipButton = ({ children, helperText }) => (
<>
<Tooltip text={helperText}>
<Button>{children}</Button>
</Tooltip>
</>
);
Liskov substitution principle
We should be able to substitute a component X with any other component Y that accepts all of X's props, without breaking the app.
E.g. Button
accepts the children prop and so does TooltipButton
. Therefore, by replacing Button
with TooltipButton
, the app should not break.
<Buttons>
<TooltipButton helperText="Execute task 1">Task 1</TooltipButton>
<TooltipButton helperText="Execute task 2">Task 2</TooltipButton>
<Button>Task 3</Button>
</Buttons>
Interface segregation principle
A component needs to accept only the props that it needs. It should not accept props only to pass them to its child components, i.e. prop drilling. The interface
keyword refers to the TypeScript interface of the component's props.
Dependency inversion principle
Dependencies should be exposed by intermediate services.
E.g. Instead of importing functions directly from a data fetching library like axios
, create an API service that exposes all the necessary functions. If ever axios
needs to be replaced with another library in the future, only the API service needs to be modified.
// api-service.js
import axios from "axios";
export function get(url, opts) {
return axios.get(url, opts);
}
export function post(url, body, opts) {
return axios.post(url, body, opts);
}
Ending note
The most important takeaway is the reasoning behind each of the above principles. However, they need to be taken with a grain of salt. In some cases, it might be up to the developer's good judgement to break some of them.
References
Eduardo Moniz's article and Ivan Kaminskyi's article have been quite helpful in understanding this topic.