Applying the Liskov Substitution Principle in React

Mohammad Faisal - Jun 27 '23 - - Dev Community

To read more articles like this, visit my blog

SOLID is a set of principles that are used as guidelines for creating a clean and maintainable application that is also less buggy and error-prone.

Today, we will take a deep dive into the third principle of SOLID: the Liskov Substitution Principle. We will try to understand how this principle can help us to create a better and cleaner React application.

Other Articles in this Series

  1. Single Responsibility Principle

  2. Open Closed Principle

  3. Interface Segregation Principle

  4. Dependency Inversion Principle

What Is the Liskov Substitution Principle?

In simple terms, this principle says:

“Subclasses should be substitutable for their superclasses.”

That means subclasses of a particular class should be able to replace the superclass without breaking any functionality.

Example

If PlasticDuck is a subclass of Duck, then we should be able to replace instances of Duck with PlasticDuck without any surprises.

Source: [Maksim Ivanov](https://maksimivanov.com/posts/liskov-substitution-principle/)

That means PlasticDuck should fulfill all the expectations set by the Duck class.

What Does This Mean in React?

React is not an object-oriented framework because it’s basically JavaScript. In the context of React, the main idea behind this principle is:

“Components should abide by some kind of contract.”

At its core, this means there should be some kind of contract between components. So whenever a component uses another component, it shouldn’t break its functionality (or create any surprises).

Let's Take a Deeper Dive

Let’s take a ModalHolder component. This component takes contentToShow as a prop and shows it inside a modal:

import {useState} from "react";
import Modal from 'react-modal';

export const ModalHolder = ({contentToShow}) => {

    const [visibility , setVisibility] = useState(false);

    return <>
        <button onClick={() => setVisibility(true)}> Show Modal</button>

        <Modal isOpen={visibility}>
           <div>{contentToShow}</div>
        </Modal>
    </>
}
Enter fullscreen mode Exit fullscreen mode

What’s the issue here?

Well, the problem is now there are no restrictions on what can be passed into the ModalHolder component. Absolutely anything can be passed into this through the variable contentToShow.

First, let’s check if our code works and everything goes as expected:

import React  , {useEffect}from'react';
import {ModalHolder} from "./views/liskov-substitution-principle/ModalHolder";

function App() {

  const modalContent = <div> This is shown inside modal </div>

  return (      
    <div>
        <ModalHolder contentToShow = {modalContent} />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Now if you open the modal, it will work just fine and show you the modal:

Show Modal

Let’s take advantage of the flaw we described earlier and see how it can destroy our application.

Let's try to pass an object into the ModalHolder and see what happens:

import React  , {useEffect}from'react';
import {ModalHolder} from "./views/liskov-substitution-principle/ModalHolder";

function App() {

  const modalContent = { key:" value" }

  return (      
    <div>
        <ModalHolder contentToShow = {modalContent} />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

This code is perfectly fine and will give no compilation error. Now let's open our application and see what happens if we click on the button:

Error

So our application is crashing even though our code has no error. What went wrong here?

Our Modal component is allowed to contain another React component. But other components are not bonded to follow that because there is no contract.

What’s the Solution?

Now we will see the importance of using TypeScript in our application and why it’s important. Let's refactor our ModalHolder component to TypeScript and see what happens:

import { ReactElement, useState } from 'react'
import Modal from 'react-modal'

interface ModalHolderProps {
    contentToShow: JSX.Element
}

export const ModalHolder = ({ contentToShow }: ModalHolderProps) => {
    const [visibility, setVisibility] = useState(false)

    return (
        <>
            <button onClick={() => setVisibility(true)}> Show Modal</button>

            <Modal isOpen={visibility}>
                <div>{contentToShow}</div>
            </Modal>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

So now we have refactored our component to accept the prop contentToShow only when it gets a JSX.Element.

If someone wants to pass anything that’s not a valid component to render, we will get an error:

Valid usage of ModalHolder

Voila! Now all other components that want to plug into the ModalHolder component need to follow a contract so that they don’t create any unexpected behavior.

Did We Do It?

We have designed our ModalHolder component in such a way that no child component that uses this component is able to create any unexpected behavior because they must abide by the rules set by the parent.

That’s exactly what Liskov Substitution Principle is all about.

So yes, we did it!

I hope you enjoyed this article as much as I enjoyed writing it.

Leave your thoughts below. And have a Great Day :D

Have something to say?

Get in touch with me via LinkedIn or Personal Website

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .