S.O.L.I.D principles with React 👨‍💻💎

Michael Ezeokoye
5 min readSep 18, 2019

--

I have found SOLID principles quite useful when thinking about Object Oriented Design. It’s a lot easier applying SOLID principles as a backend developer when your working with classes, because classes are the blueprint for creating objects.

Image taken from http://www.graybaresp.com

With the addition of Hooks in React 16.8 that lets one use state and other react features without writing a class, i’ve become fond of react. Exploring the different design patterns, working with hooks and stateless functional components.

I find react function components much easier to read and test because they are plain JavaScript functions without state or lifecycle-hooks. Pairing it with Hooks offers a way to reuse stateful logic without changing your component hierarchy and extract stateful logic from a component so it can be tested independently. And when writing react in Typescript, files can be upgraded incrementally without raising issues at runtime.

Let’s see how we can build a data-table with search, sort and pagination by following the SOLID principles with react. We’ll be building the data-table off my previous post Tailwind + React TypeScript = 🔥👌😎.

We’ll start off with a simple design of our data-table component with Tailwind-css and import font awesome for the icons. Switch to the project directory on your terminal and run the command below:

yarn add @fortawesome/fontawesome-free -D

Now that we have font awesome imported, we can go ahead and style the data-table with tailwind-css.

Datatable.tsx

For the sake of simplicity, I will not provide the full implementation of the Datatable but you can get the source code on Github on the datatable branch.

Single responsibility principle

A class should have one, and only one, reason to change.

Components should have a specific purpose, hence having small presentational components is encouraged.

In the context of Object Oriented Design we can see the Datatable component as an object that will be called by many pages because of its features. We achieve single responsibility by making the Datatable component a presentational component that receives data and callbacks as props only, provided by its parent component. For starters a dispatch function for it’s actions, the table data and pagination properties to render will be passed to the data-table component as props, by so doing the component rerenders when the value of these props change.

Open-closed principle

It should be possible to extend the behavoir of a class without modifying it.

A component should be able to use higher order functions or render props to extend it’s behaviour

That means it should be possible to extend the behaviour of the datatable component without modifying it. Imagine we have a table of name and gender and we want to do something with the gender like displaying 👨 for male and 👩 for female or whatever.

We can’t start editing the datatable component for these wild scenarios, but by using render callbacks or render props we can share the logic to the component like this.

<tbody>
{
currentData.map((row, index) =>
<tr key={index} className="hover:bg-gray-300">
{
header.map(col =>
<td key={col.key} className="py-4 px-6 border-b border-gray-500 text-center">
{col.cell ? col.render(row) : row[col.key]}
</td>
)
}
</tr>
)
}
</tbody>

currentData and header are props that will be passed into the component.

The render props passed in the header will be :

const header: Header[] = [
{
title: "Name",
sortable: true,
key: "name"
},
{
title: "Gender",
sortable: false,
key: "gender",
render: (row: any) => row.gender === "male" ? "👨" : row.gender === "female" ? "👩" : "😕"
}
]

Liskov Substitution principle

Subclasses should be substitutable for their superclasses.

Components, hooks and functions should abide to some type of Interface contract.

One of the benefit of writing Typescript is the ability to define types and create interfaces. The types and interfaces serve as a contract between a components and it’s props or a functions and its parameters.

When we define currentData as any[] and header as Header[] , the Header interface is defined as:

interface Header {
title: string,
sortable: boolean,
render?: Function,
key: string
}

By so doing an interface contract has been created between the component and the props passed from the parent component about the minimum requirement for the currentData and header props.

Interface segregation principle

Many small, client-specific interfaces are better than one general purpose interface.

Specifying the smallest set of data required by a component or function.

Requiring the least specific interface to the data is good practice. Why use Lists of ints when Enumerations of T work just as well?

Pagination, search, and sort are events that occur when a user interacts with the datatable. This can be translated into actions that can be dispatched by the component when a user interacts with it. Specific actions of the data-table can be defined as enum Actions {SEARCH, SORT, PAGE} with action types:

type ActionTypes =
| { type: Actions.SEARCH, key: string }
| { type: Actions.SORT, key: string }
| { type: Actions.PAGE, key: number }

These can be exported and used to build reducers with different implementations. For example we could have an implementation for client side and server side data-table. Specific props neccessary for pagination needs to be passed into the component. We can define an interface for it as :

interface Pagination {
currentData: any[],
pages: any[],
lastPage: number,
currentPage: number
}

and finally the specific props needed for the component can be defined as:

interface DatatableInterface extends Pagination {
dispatch: React.Dispatch<ActionTypes>,
header: Header[]
}

Dependency inversion principle

Depends on abstractions not concretions.

Specifying parameters to a function or component rather than hard coding it to go get some value.

When implementing client side datatable, once SEARCH and SORT actions are performed on the data, the data has to be paginated. Before sending it we can create a function that implements the Pagination interface as follows.

now when search or sort is performed on the data we can return the pagination function with right parameters, the result is an implementation of the Pagination interface.

Thanks to Robert Martin who first conceptualized SOLID principles in his 2000 paper, Design Principles and Design Patterns, and Michael Feathers who later built on these concepts and introduced us to the SOLID acronym.

By applying these principles, we can develop softwares that are easy to maintain and extend in an agile manner.

Happy Coding ✌

--

--