SOLID Design Principles - React

ℹ️
This post is greatly inspired by Joshua Dysiewicz's and Konstantin Lebedev's posts that expand SOLID design principles of the object-oriented programming paradigm to functional React frontend. I tried to recreate their works in my own terms by refactoring the code of my past project.

In the following code, I've extracted a sub-module from one of my React project(🔗 GitHub) and simplified some of the logics and substituted server data with mock data. The module fetches list of posts and displays them.

This innocent-looking app already violates a lot of SOLID principles and is written in a way that hinders software from being functional, maintainable, and robust. At the same time, some of the well-established patterns of React encourage us to follow SOLID principles unwittingly. Let's look at which of the rules are being observed and the others are not.

Single Responsibility Principle

SRP states that "a module should have one, and only one, reason to change." Consider <Posts /> component in the example. It

  • fetches data from server
  • and displays the fetched data

at the same time. Its responsibility is not cohesive and has more than one reason to change. For example, whenever the interface of server response changes (e.g. { posts: [] }{ results: [] }), when we want to add another column to the table, etc., <Posts /> is affected.

How does <Posts /> that appeases SRP look like?

Now, every module (not only file-level module but also functions and data structure) has one, cohesive responsibility.

  • usePosts returns hooks array that fetches post data from the server. I've separated useEffect hooks from <Posts /> and made a custom hooks module.
  • columns defines which portion of the fetched data should be displayed.
  • <Posts /> displays posts in a table view, which consists of a table header and rows.
  • <Post /> displays post items in a row view, which consists of columns for posts' id, title, and user id.
  • StyledPosts adds style rules to <Posts />

Now each function and data structure participates either in fetching data or in visualizing data, not both.

Interface Segregation Principle

ISP states that a module should avoid depending on things that they don’t use. Joshua Dysiewicz's post concisely bends the principle for React code: "only pass a component props it needs." The statement seems to be justifiable since, in React application, a component is rendered every time the prop's value passed to it by its parent component changes. One can say that props is a sort of dependency.

In the above code, <Post />  doesn't exhaust the props it receives. The interface of server response looks as follows:

{
    posts : {
        id: number
        title: string
        body: string
        userId: number
        tags: string[],
        reactions: number
    }[]
}

While the entire post object with 6 keys are passed to each <Post />  as props, <Post /> accesses to only 3 keys(`id`, title, body).

Why is not using the entirety of props object a problem? Because it means that the child component has to make decisions as to how to utilize its dependency. This is not desirable because whenever the server changes the interface of the post object, both parent(<Posts />) and child(<Post />) component have to be modified. Imagine, for example, that the server decides to add user's name to post and group user's name and id into one object. Now, the response to /posts request would look like this:

{
    posts: {
        id: number
        title: string
        body: string
        user: {
            id: number
            name: string
        }
        tags: string[]
        reactions: number
    }[]
}

Now, in order for the app to not break, we have to change the source code of <Post /> to look at post.user.id instead of post.userId. However, according to how we have rewritten <Post /> component in the above SRP section, <Post />'s one and only responsibility is to display post data in a 3-column format.  How the original interface of post  is structured isn't and shouldn't be its concern. Nonetheless, the code as it is now forces <Post /> to be aware of the implementation detail in order to accomplish a single job.

ℹ️
According to Vladimir Khorikov's article, a nicely encapsulated and abstracted public API accomplishes the task from the outer layer(client code) in a single invocation. Meanwhile, implementation detail is an intermediate step that a public API calls internally to achieve the goal. In <Post />'s perspective, post is a public API. How the data is retrieved or structured is part of the implementation detail. Such implementation detail should be handled by <Posts />, which exposes the API to <Post />.

Let's amend the code:

Now, if we were to change post's interface to nested one, we only need to change <Posts /> like the following and leave <Post /> intact.

const Posts = () => {
  // (...)
  return (
    <StyledPosts>
      // (...)
      <tbody>
        {posts.map(({ user, id, title }, postIdx) => (
          <Post key={postIdx} post={{ userId: user.id, id, title }} />
        ))}
      </tbody>
    </StyledPosts>
  );
};

Dependency Inversion Principle

DIP is strongly connected to SRP and ISP. A lot of times, the observation of one rule is a natural corollary to that of another. It is true for our example too. The way we declared custom hooks in SRP section also promotes DIP. By importing usePosts module, the way post data are fetched from the server is abstracted from <Posts />.  As a consequence, the two modules are loosely coupled. usePosts can be easily replaced with another module with the same interface. For example, it becomes easier to do unit testing, since we only have to manipulate useTodos to retrieve mock data from local repository and make sure that it returns an array of objects with the same data structure.

Open-Closed Principle

When using function components, Render Props Pattern, and component composition, React developers are most likely to abide by the OCP principle already.

Here's a rather contrived example that intentionally goes against OCP.

I've refactored <Posts /> in a class component style. Then, in order to separate the container component that fetches server data from the presentational component, I had <PostsView /> inherit <PostsController/>. The two components are tightly coupled. If <PostsController /> changes the name of its state data( posts) to something else, it inevitably results in modifying the source code of <PostsView />.

On the contrary, how custom hooks usePosts names its internal state doesn't have to match how <Posts /> call it.

Even if usePosts calls the post array items , <Posts /> can still call the returned value of usePosts() posts. In this regard, React hooks pattern is designed with great consideration. By returning state and setState function in an array(e.g. [posts, setPosts]), the way they are called in the place of invocation and of declaration doesn't have to match. On the other hand, if they were to be returned as an object(e.g. { posts, setPosts }), how they are called does matter across modules.

Liskov Substitution Principle

LSP states that if type S is a subtype of type T, the substitution of all objects of type T with those of type S should not alter a software program's behavior. At first glance, the principle appears to be less relevant to JavaScript, to a programming language that has neither interface nor type. However, Robert Martin says that while LSP has been "thought of (...) as a way to guide the use of inheritance, (...) over the years [it] has morphed into a broader principle of software design." As a matter of fact, Konstantin Lebedev says that "in a broader sense, inheritance is simply basing one object upon another object while retaining a similar implementation, and this is something we do in React quite often."

He continues that "a very basic example of a subtype/supertype relationship could be demonstrated with a component built with styled-components library(or any other CSS-in-JS library that uses similar syntax)." In our example, <StyledPosts /> simply adds style to a regular HTML <table> element without modifying any of <table>'s interface. <StyledPosts /> can be considered a subtype of <table />. Wherever <table /> is used, it can be substituted by <StyledPosts />. Thus, applying style rules to a component by creating a new, styled component conforms to LSP.

References

Subscribe to go-kahlo-lee

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe