Using the Dependency Inversion Principle (DIP) in React

Using the Dependency Inversion Principle (DIP) in React

With code example

Introduction

The SOLID principles have continually offered a foundation for efficient, scalable, and maintainable software in the constantly changing world of software development. The Dependency Inversion Principle (DIP), one of these principles, is essential for decoupling software modules, increasing their modularity and testability.

React is a cutting-edge UI toolkit that focuses on component-based programming, so it begs the question: How can DIP be utilized effectively in React?

Let's find out in this blog.

What is the Dependency Inversion Principle?

At its core, DIP imparts two essential guidelines:

  • Low-level modules shouldn't be relied upon by high-level modules, which define complicated processes and workflows. Both ought to be dependent upon abstractions (such as interfaces or abstract classes).

  • Detail-based abstractions should not be used. Instead, details ought to be constructed based on these abstractions. To put it another way, high-level modules and low-level modules should communicate via intermediary abstractions rather than directly connecting them. This method increases flexibility by reducing direct dependency between components.

Benefits of Applying DIP in React

  1. Enhanced Flexibility: With components not directly tied to specific implementations, changes in one part of an application have a reduced risk of unintentionally affecting other parts.

  2. Improved Testability: Components that depend on abstractions can easily be tested by mocking these abstractions.

  3. Increased Modularity: When components are decoupled, they can be developed, tested, and scaled independently, promoting a more modular application architecture.

Applying DIP in React Components: Good vs. Bad Approach

Bad Approach

index.tsx

import { Form } from "./form";

export function DIP() {
   return <Form />;
}

Form.tsx

import axios from "axios";
import React, { useState } from "react";

interface IFormProps {
}

export function Form(props: IFormProps) {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");


   const handleSubmit = async (e: React.FormEvent) => {
     e.preventDefault();

     await axios.post("https://localhost:3000/login", {
      email,
       password,
    });
  };

  return (
    <section>
      <div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
        <div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
          <div className="p-6 space-y-4 md:space-y-6 sm:p-8">
            <h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
              Sign in to your account
            </h1>
            <form
              className="space-y-4 md:space-y-6"
              onSubmit={handleSubmit}
            >
              <div>
                <label
                  htmlFor="email"
                  className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
                >
                  Your email
                </label>
                <input
                  type="email"
                  name="email"
                  value={email}
                  onChange={(e) => setEmail(e.target.value)}
                  id="email"
                  className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
                  placeholder="name@company.com"
                />
              </div>
              <div>
                <label
                  htmlFor="password"
                  className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
                >
                  Password
                </label>
                <input
                  type="password"
                  name="password"
                  value={password}
                  onChange={(e) => setPassword(e.target.value)}
                  id="password"
                  placeholder="••••••••"
                  className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
                />
              </div>
              <div className="flex items-center justify-between">
                <div className="flex items-start">
                  <div className="flex items-center h-5">
                    <input
                      id="remember"
                      aria-describedby="remember"
                      type="checkbox"
                      className="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-primary-600 dark:ring-offset-gray-800"
                    />
                  </div>
                  <div className="ml-3 text-sm">
                    <label
                      htmlFor="remember"
                      className="text-gray-500 dark:text-gray-300"
                    >
                      Remember me
                    </label>
                  </div>
                </div>
                <a
                  href="#"
                  className="text-sm font-medium text-primary-600 hover:underline dark:text-primary-500"
                >
                  Forgot password?
                </a>
              </div>
              <button
                type="submit"
                className="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
              >
                Sign in
              </button>
              <p className="text-sm font-light text-gray-500 dark:text-gray-400">
                Don’t have an account yet?{" "}
                <a
                  href="#"
                  className="font-medium text-primary-600 hover:underline dark:text-primary-500"
                >
                  Sign up
                </a>
              </p>
            </form>
          </div>
        </div>
      </div>
    </section>
  );
}

The provided code shows a component named Form that plays the role of rendering a login interface and, simultaneously, handling the data submission to a server using axios. Let's break down the logic:

  1. State Management: The component internally manages the state for email and password using React's useState hook.

  2. Rendering: The component renders a form layout, capturing email, password, and a 'remember me' checkbox.

The non-adherence to the Dependency Inversion Principle manifests in a few ways:

  1. Tight Coupling with Axios: The component directly uses the axios library for API calls. This makes the component tightly coupled with the axios library. If you later decide to switch to another library or change the method of data submission, this component will need to be rewritten or significantly modified.

  2. Mixed Responsibilities: The component handles both UI rendering and business logic (API request). This amalgamation of duties breaches the Single Responsibility Principle, another SOLID principle. While DIP isn't about splitting responsibilities per se, adherence to DIP often results in cleaner, more focused components because dependencies are abstracted out.

  3. Lack of Abstraction for Data Submission: In a DIP-adherent approach, the Form component would receive a function (through props or context) that handles data submission. This way, the component would be decoupled from the specifics of how data is submitted. In the given code, there is no such abstraction.

Good Approach

index.tsx

import { ConnectedForm } from "./connectedForm";

export function DIP() {
  return <ConnectedForm />;
}

ConnectedForm.tsx

import axios from "axios";
import { Form } from "./form";

export function ConnectedForm() {
  const handleSubmit = async (email: string, password: string) => {
    await axios.post("https://localhost:3000/login", {
      email,
      password,
    });
  };
  return <Form onSubmit={handleSubmit} />;
}

Form.tsx

import axios from "axios";
import React, { useState } from "react";

interface IFormProps {
  onSubmit: (email: string, password: string) => void;
}

export function Form(props: IFormProps) {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const { onSubmit } = props;

  const handleSubmit = (e: React.FormEvent) => {
    onSubmit(email, password);
  };

  return (
    <section>
      <div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
        <div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
          <div className="p-6 space-y-4 md:space-y-6 sm:p-8">
            <h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
              Sign in to your account
            </h1>
            <form
              className="space-y-4 md:space-y-6"
              onSubmit={handleSubmit}
            >
              <div>
                <label
                  htmlFor="email"
                  className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
                >
                  Your email
                </label>
                <input
                  type="email"
                  name="email"
                  value={email}
                  onChange={(e) => setEmail(e.target.value)}
                  id="email"
                  className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
                  placeholder="name@company.com"
                />
              </div>
              <div>
                <label
                  htmlFor="password"
                  className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
                >
                  Password
                </label>
                <input
                  type="password"
                  name="password"
                  value={password}
                  onChange={(e) => setPassword(e.target.value)}
                  id="password"
                  placeholder="••••••••"
                  className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
                />
              </div>
              <div className="flex items-center justify-between">
                <div className="flex items-start">
                  <div className="flex items-center h-5">
                    <input
                      id="remember"
                      aria-describedby="remember"
                      type="checkbox"
                      className="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-primary-600 dark:ring-offset-gray-800"
                    />
                  </div>
                  <div className="ml-3 text-sm">
                    <label
                      htmlFor="remember"
                      className="text-gray-500 dark:text-gray-300"
                    >
                      Remember me
                    </label>
                  </div>
                </div>
                <a
                  href="#"
                  className="text-sm font-medium text-primary-600 hover:underline dark:text-primary-500"
                >
                  Forgot password?
                </a>
              </div>
              <button
                type="submit"
                className="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
              >
                Sign in
              </button>
              <p className="text-sm font-light text-gray-500 dark:text-gray-400">
                Don’t have an account yet?{" "}
                <a
                  href="#"
                  className="font-medium text-primary-600 hover:underline dark:text-primary-500"
                >
                  Sign up
                </a>
              </p>
            </form>
          </div>
        </div>
      </div>
    </section>
  );
}

The improved approach divides the code into three main parts: index.tsx, ConnectedForm.tsx, and Form.tsx. Let's dive into each section to understand how it adheres to the Dependency Inversion Principle (DIP):

1. Separation of Concerns:

The components are now separated based on their responsibilities:

  • Form.tsx: Primarily focuses on the UI and rendering of the form.

  • ConnectedForm.tsx: Acts as a bridge between Form and the data layer, managing the logic to submit form data.

This clear separation simplifies the understanding and maintenance of each component.

2. Form Component (Form.tsx):

The form component takes a function onSubmit as a prop and doesn't concern itself with what happens when form data is submitted. Its main responsibilities are:

  • Managing local state for the email and password.

  • Rendering the UI for the form.

By expecting onSubmit as an external dependency (passed as a prop), the form component adheres to the DIP. It relies on abstractions and is not tied to any particular implementation of data submission.

3. ConnectedForm Component (ConnectedForm.tsx):

This component integrates the business logic (in this case, making an API call) with the UI component:

  • It imports the axios library to handle the API request.

  • It defines the handleSubmit function, detailing how to submit form data using axios.

  • It renders the Form component and passes handleSubmit to it as the onSubmit prop.

Here, the DIP is observed by keeping the data-fetching logic separate from the UI logic. This abstraction makes the Form component more versatile and allows for easier modifications in the future (e.g., changing the API endpoint or method of data submission).

4. High-Level and Low-Level Modules:

Following the DIP, the high-level Form module doesn't depend on the low-level data submission details. Instead, ConnectedForm acts as an intermediary, providing the required low-level functionality to the high-level Form through props.

5. Flexibility and Testability:

With this structure, you can easily change the data submission logic without altering the UI logic and vice-versa. This separation also aids in testing:

  • The Form component can be tested independently, mocking the onSubmit function.

  • The data submission logic in ConnectedForm can be tested separately, without the need to render the entire form UI.

Conclusion

Integrating the Dependency Inversion Principle within React applications offers a promising route to more modular, flexible, and maintainable software. By understanding and effectively applying DIP, developers can not only harness the power of React but also ensure that their applications stand the test of time, adapting to changes with grace and ease.

Did you find this article valuable?

Support Adeesh's Software Engineering Insights by becoming a sponsor. Any amount is appreciated!