Dependency Inversion Principle (DIP) in React

Chhakuli Zingare - Mar 3 - - Dev Community

Welcome back to our SOLID principles in the React series! So far, we’ve covered:

S: Single Responsibility Principle (SRP): Keep components focused.

O: Open/Closed Principle (OCP): Make extensions easy, not modifications.

L: Liskov Substitution Principle (LSP): Keep components interchangeable without breaking behavior.

I: Interface Segregation Principle (ISP): Keep your React components and interfaces lean, modular, and easier to maintain.

In today's blog, let's talk about something that can dramatically improve your React applications: the Dependency Inversion Principle (DIP). Don't worry if it sounds complicated by the end of this post, you'll have a practical understanding of how to use it in your React projects.

What's DIP in simple words?

The Dependency Inversion Principle has two main parts:

  1. High-level components shouldn't depend on low-level components. Both should depend on abstractions.

  2. Abstractions shouldn't depend on details. Details should depend on abstractions.

In React terms, think of it this way: instead of having your components directly depend on specific implementations (like a particular API service or UI library), they should depend on interfaces or props that define what they need. This makes your components more flexible and easier to change.

Why Dependency Inversion Matters in React

Applying DIP in React can help with:

  • Better component reusability: Separating UI logic from business logic makes components more reusable across different parts of the app.

  • Easier testing: Components relying on abstractions (props, context, hooks) instead of direct API calls are easier to test using mocks.

  • Improved maintainability: When components depend on abstractions instead of concrete implementations, swapping logic (e.g., changing an API call) doesn't require rewriting UI components.

  • Clearer separation of concerns: Business logic stays in services or hooks, while components focus on rendering UI.

Now, let’s see a bad example and refactor it using DIP.

A Bad Example: Tight Coupling Between UI and Business Logic

Consider this React component that fetches user data and displays it:

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

const UserProfile: React.FC = () => {
  const [user, setUser] = useState<{ name: string; email: string } | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch("https://api.example.com/user")
      .then((res) => res.json())
      .then((data) => {
        setUser(data);
        setLoading(false);
      });
  }, []);

  if (loading) return <p>Loading...</p>;
  return <div>{user ? <h2>{user.name}</h2> : <p>No user found</p>}</div>;
};

export default UserProfile;
Enter fullscreen mode Exit fullscreen mode

What’s Wrong Here?

  • Direct API call inside the component: This makes it hard to reuse UserProfile elsewhere with different data sources.

  • Difficult to test: Since fetch is inside the component, mocking API responses becomes a hassle.

  • Tightly coupled logic: UI and data fetching are tightly bound, violating the separation of concerns.

Applying Dependency Inversion: Refactoring with Hooks

We can improve this by moving data-fetching logic into a separate hook and passing the data as props.

Step 1: Create a Custom Hook for Data Fetching

import { useEffect, useState } from "react";

export const useUserData = () => {
  const [user, setUser] = useState<{ name: string; email: string } | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch("https://api.example.com/user")
      .then((res) => res.json())
      .then((data) => {
        setUser(data);
        setLoading(false);
      });
  }, []);

  return { user, loading };
};
Enter fullscreen mode Exit fullscreen mode

Step 2: Use Dependency in the Component

import React from "react";

interface UserProfileProps {
  user: { name: string; email: string } | null;
  loading: boolean;
}

const UserProfile: React.FC<UserProfileProps> = ({ user, loading }) => {
  if (loading) return <p>Loading...</p>;
  return <div>{user ? <h2>{user.name}</h2> : <p>No user found</p>}</div>;
};

export default UserProfile;
Enter fullscreen mode Exit fullscreen mode

Step 3: Compose Components Together

import React from "react";
import UserProfile from "./UserProfile";
import { useUserData } from "./useUserData";

const UserProfileContainer: React.FC = () => {
  const { user, loading } = useUserData();
  return <UserProfile user={user} loading={loading} />;
};

export default UserProfileContainer;
Enter fullscreen mode Exit fullscreen mode

Benefits of This Refactor

  • UserProfile is now independent of the data-fetching logic and can be used anywhere.

  • useUserData can be easily mocked in tests.

Common Mistakes to Avoid

  1. Over-abstraction: Don’t create unnecessary interfaces/hooks unless they add real flexibility.

  2. Still relying on direct API calls in UI components: Always separate concerns properly.

  3. Skipping dependency injection in favor of global state everywhere: Context or Redux can help, but they should not be the default choice for everything.

  4. Not considering reusability: Ask yourself if this component needed different data tomorrow, would it still work?

When to Apply DIP in Real Projects

  • Reusable UI Components: Components that need different data sources (e.g., UserProfile, ProductCard).

  • Testing Flexibility: When API responses or logic should be mocked easily.

  • Scalable Applications: If the project is growing, keeping UI and logic separate prevents future headaches.

Conclusion

The Dependency Inversion Principle (DIP) is all about making React components more maintainable, testable, and reusable. By moving business logic into hooks/services and injecting dependencies via props, we can keep our components lean and flexible.

Key Takeaways:

  • Avoid direct API calls inside components.

  • Use hooks or services for business logic.

  • Pass dependencies via props instead of hardcoding them.

  • Keep UI components focused on rendering, not logic.

Applying DIP might seem like extra work at first, but it pays off in the long run with better scalability and maintainability. Start small and apply it where it makes sense!

And with that, our SOLID series comes to an end—but there's much more to come! Stay tuned for more blogs.

If you found this helpful, share it with your team and improve your React codebase together! 🚀


We at CreoWis believe in sharing knowledge publicly to help the developer community grow. Let’s collaborate, ideate, and craft passion to deliver awe-inspiring product experiences to the world.

Let's connect:

This article is crafted by Chhakuli Zingare, a passionate developer at CreoWis. You can reach out to her on X/Twitter, LinkedIn, and follow her work on the GitHub.

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