Using the Dependency Inversion Principle (DIP) in React
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
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.
Improved Testability: Components that depend on abstractions can easily be tested by mocking these abstractions.
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:
State Management: The component internally manages the state for
email
andpassword
using React'suseState
hook.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:
Tight Coupling with Axios: The component directly uses the
axios
library for API calls. This makes the component tightly coupled with theaxios
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.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.
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 betweenForm
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
andpassword
.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 usingaxios
.It renders the
Form
component and passeshandleSubmit
to it as theonSubmit
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 theonSubmit
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.
Subscribe to my newsletter
Read articles from Adeesh Sharma directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Adeesh Sharma
Adeesh Sharma
Adeesh is an Associate Architect in Software Development and a post graduate from BITS PILANI, with a B.E. in Computer Science from Osmania University. Adeesh is passionate about web and software development and strive to contribute to technical product growth and decentralized communities. Adeesh is a strategic, diligent, and disciplined individual with a strong work ethic and focus on maintainable and scalable software.