DEV Community

Zahid Hasan
Zahid Hasan

Posted on

Liskov Substitution Principle in React

What is Liskov Substitution Principle?

The Liskov Substitution Principle (LSP) is a guideline that assists in the effective functioning of the software. It states that if something is of a given kind, you should be able to substitute a different but similar item for it and the software would still function properly.

Consider a toy box filled with various sorts of toys such as vehicles, dolls, and balls. Because the toy box can only contain toys, whatever is placed within it must be a toy. According to the LSP, if you have a toy car, you should be able to replace it with a toy truck and the toy box should still function properly.

According to the LSP, objects of a superclass should be able to be substituted by objects of a subclass without impacting the program’s correctness.

How can you implement Liskov Substitution Principle in React?

Here are some ways to implement the Liskov Substitution Principle in React:

  • Make your own hooks to specify the anticipated behavior and features of certain functionality, such as a form input. Any component that makes use of the custom hook should be able to be utilized whenever a form input is anticipated and the application should still function properly.
  • Create custom hooks that expect a set of properties, such as an object containing the component’s state and behavior, then return the desired behavior and properties. As long as the items have the same form, this enables for easy substitution of various objects with varying actions.
  • Use composition to define the expected behavior and properties of a component. Composing small and focused components together, it allows for easy substitution of related components that conform to the same interface and behavior.

Here’s an example of a scenario where the Liskov Substitution Principle is violated:

Assume you have a custom hook named useAuth that controls an application’s authentication state, such as the user’s token and login status.

import { useState } from "react";

const useAuth = () => {
  const [token, setToken] = useState(null);
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  const login = (token) => {
    setToken(token);
    setIsLoggedIn(true);
  }

  const logout = () => {
    setToken(null);
    setIsLoggedIn(false);
  }

  return { token, isLoggedIn, login, logout };
}
Enter fullscreen mode Exit fullscreen mode

Suppose you have a component named Profile that renders the user’s information while they are signed in and a login button when they are not.

const Profile = () => {
  const { token, isLoggedIn, login } = useAuth();

  if (!isLoggedIn) return <button onClick={() => login("pass")}>Login</button>;

  return <p>Welcome, you are logged in.</p>;
}
Enter fullscreen mode Exit fullscreen mode

Imagine you want to add another component named Settings that allows the user to update their settings while they are logged in and a login button when they are not.

const Settings = () => {
  const { token, isLoggedIn, login } = useAuth();

  if (!isLoggedIn) return <button onClick={() => login("pass")}>Login</button>;

  return (
    <div>
      <form>
        <label>Change your password</label>
        <input type="password" placeholder="New password" />
        <button>Save</button>
      </form>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

As you can see, both the Profile and Settings components utilize the useAuth hook to manage the application’s authentication state, but because each component has distinct expectations for the hook, the useAuth hook is unable to handle the various cases. Because the useAuth hook cannot be used interchangeably in various components, this violates the LSP.

To address this issue, you could construct different hooks for different use cases, or you could develop a more general hook that can handle multiple circumstances by giving more parameters such as anticipated behavior or data.

Following this principle allows you to design a collection of interchangeable hooks, which makes it simple to add new features to your application because you can use the same hooks for other components and the application will still function appropriately.

Here is one solution to fix the issue of the useAuth hook violating the LSP:

Create a more general useAuth hook that can handle several cases by giving additional arguments, such as desired behavior or data.

import { useState } from "react";

const useAuth = (initialAuth) =>{
  const [auth, setAuth] = useState(initialAuth);

  const login = (token) => {
    setAuth({ token, isLoggedIn: true });
  }

  const logout = () => {
    setAuth({ token: null, isLoggedIn: false });
  }

  return { auth, login, logout };
}
Enter fullscreen mode Exit fullscreen mode

Pass the initial auth state as a parameter to the useAuth hook.

const Profile = () => {
  const { auth, login } = useAuth({ token: null, isLoggedIn: false });

  if (!auth.isLoggedIn)
    return <button onClick={() => login("pass")}>Login</button>;

  return <p>Welcome, you are logged in.</p>;
};

const Settings = () => {
  const { auth, login } = useAuth({ token: null, isLoggedIn: false });

  if (!auth.isLoggedIn)
    return <button onClick={() => login("pass")}>Login</button>;

  return (
    <div>
      <form>
        <label>Change your password</label>
        <input type="password" placeholder="New password" />
        <button>Save</button>
      </form>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

The useAuth hook may handle multiple scenarios and be used interchangeably in other components by giving the initial auth state as a parameter. This enables the components to have varied expectations for the hook while still functioning appropriately, in accordance with the Liskov Substitution Principle.

Top comments (0)