Skip to content

SOLID principles in JavaScript frameworks

Magma on a mountain

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.