Asher Cohen
Back to posts

Dependency Injection in React

Leverage React's built-in dependency injection patterns for more testable, maintainable code

Introduction

Dependency injection (DI) is a design pattern where dependencies are provided to a component rather than created internally. React has built-in support for DI through props and Context, making it easy to write testable, maintainable code.

What is Dependency Injection?

Instead of creating dependencies inside a component, you pass them in:

// ❌ Without DI - hard to test
function UserService() {
  const api = new ApiClient(); // Hard-coded dependency

  const getUser = async (id) => {
    return await api.fetch(`/users/${id}`);
  };
}

// ✅ With DI - easy to test
function UserService({ api }) {
  const getUser = async (id) => {
    return await api.fetch(`/users/${id}`);
  };
}

React's Built-In DI: Props

Props are the simplest form of dependency injection in React.

function UserList({ users, onUserClick }) {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id} onClick={() => onUserClick(user)}>
          {user.name}
        </li>
      ))}
    </ul>
  );
}

// Inject dependencies via props
<UserList users={fetchedUsers} onUserClick={handleUserClick} />;

React Context for DI

Context provides a way to share dependencies across the component tree without prop drilling.

// Create context
const ApiContext = createContext(null);

// Provide dependency
function App() {
  const api = new ApiClient();

  return (
    <ApiContext.Provider value={api}>
      <UserList />
    </ApiContext.Provider>
  );
}

// Consume dependency
function UserItem({ userId }) {
  const api = useContext(ApiContext);
  const [user, setUser] = useState(null);

  useEffect(() => {
    api.getUser(userId).then(setUser);
  }, [userId, api]);

  return <div>{user?.name}</div>;
}

Custom Hooks for DI

Custom hooks can encapsulate dependency injection logic.

function useApi() {
  return useContext(ApiContext);
}

function useUser(userId) {
  const api = useApi();
  const [user, setUser] = useState(null);

  useEffect(() => {
    api.getUser(userId).then(setUser);
  }, [userId, api]);

  return user;
}

// Clean component with injected dependency
function UserProfile({ userId }) {
  const user = useUser(userId);
  return <div>{user?.name}</div>;
}

Testing with DI

Dependency injection makes testing easier by allowing you to mock dependencies.

// Mock API for testing
const mockApi = {
  getUser: jest.fn().mockResolvedValue({ id: 1, name: 'Test' }),
};

// Test with injected mock
render(
  <ApiContext.Provider value={mockApi}>
    <UserProfile userId={1} />
  </ApiContext.Provider>,
);

expect(mockApi.getUser).toHaveBeenCalledWith(1);

Best Practices

Inject at the Top Level

// ✅ Good - inject at app root
function App() {
  const api = createApi();
  const auth = createAuth();

  return (
    <ApiContext.Provider value={api}>
      <AuthContext.Provider value={auth}>
        <MainApp />
      </AuthContext.Provider>
    </ApiContext.Provider>
  );
}

// ❌ Bad - inject deep in tree
function ChildComponent() {
  const api = createApi(); // Creates new instance every render
}

Use Multiple Contexts

Separate concerns with multiple context providers.

<ApiContext.Provider value={api}>
  <AuthContext.Provider value={auth}>
    <ThemeContext.Provider value={theme}>
      <App />
    </ThemeContext.Provider>
  </AuthContext.Provider>
</ApiContext.Provider>

Document Dependencies

/**
 * UserProfile Component
 * @param {Object} props
 * @param {number} props.userId - User ID to display
 * @param {Object} props.api - API client (injected via context)
 */
function UserProfile({ userId }) {
  const api = useContext(ApiContext);
  // ...
}

Conclusion

React's props and Context provide powerful dependency injection capabilities. Use them to write more testable, maintainable components.

#react #dependency-injection #architecture