DEV Community

Lucas Brogni
Lucas Brogni

Posted on

Removing code smells: Using dependency injection through Props in React

Have you ever got to jumped into a React application and looked at some components and thought it was so hard to test?

How could you do it, if it had the API client imported to the file, and the dependency was not managed by you?

Well, one can say you can use mocks, and that would solve the problem. Yes, at some level this is true, but on the other hand, your test becomes much more complex, and as the application grows it gets harder and harder to test.

Well, here comes a much better solution for it: Use dependency injection through the component properties.

If you don't know what dependency injection is then a quick definition:

Dependency Injection is a technique used to achieve Inversion of Control by passing (injecting) an object’s dependencies rather than having the object create them itself. The goal of DI is to decouple the creation of objects from their usage, making the system more modular, easier to test, and easier to maintain. DI can be done in various ways, such as constructor injection, setter injection, or interface injection, each providing different levels of flexibility and control.
-Robert C. Martin - Clean Code: A Handbook of Agile Software Craftsmanship

In react there are a few ways for achieving it, but the easiest and probably the cleaner is using the props to pass it.

You might want to use the context to avoid props drilling, but that's the subject of another article.

Ok, how to implement it then? Well, here comes the code.

Let's say you have a Todo application and want to fetch a list of Todos.

For that, you will implement an interface of the API so we not only use Dependency Injection, but also follow the Dependency Inversion principle.

// src/api/ITodoAPI.ts
export interface ITodoAPI {
  getTodos(): Promise<Todo[]>;
}
Enter fullscreen mode Exit fullscreen mode

As we are using typescript, we are going to also declare, our Todo type.

// src/types.tsx
export type Todo = {
  id: number;
  title: string;
  completed: boolean;
}
Enter fullscreen mode Exit fullscreen mode

With both interface and types defined, we can then implement our API Class. This is the place where your dependencies will land. If you want to use Axios, feel free to import it here, if you have another dependency, it does not have a problem to import, as this will be your Adapter for the outside world. Please note, that this class has no connection with the TodoList component whatsoever.

// src/api/TodoAPI.ts
import { ITodoAPI } from './ITodoAPI';
import { Todo } from '../types.tsx'; 
import * as axios from 'axios'; 

export class TodoAPI implements ITodoAPI {
  private URL = 'https://should-be-in-the-env.com';

  async getTodos(): Promise<Todo[]> {
    const result = await axios.get(`${this.URL}/todos`);
    return result.data;
  }
}

Enter fullscreen mode Exit fullscreen mode

When you go the development of the component itself, the most important thing will be defining that your component will receive via Prop a todoAPI property with type of ITodoAPI so, it makes your component wait to receive the API and allows it to be used later.

// src/components/TodoList.tsx
import React from 'react';
import { ITodoAPI } from '../api/ITodoAPI';

interface TodoListProps {
  todo API: ITodoAPI;
}

const TodoList: React.FC<TodoListProps> = ({ todoAPI }) => {

  return (
    <></>
  );
})
Enter fullscreen mode Exit fullscreen mode

With all the setup ready, then it's time for the actual implementation of the component.

// src/components/TodoList.tsx
import React, { useEffect, useState } from 'react';
import { ITodoAPI } from '../api/ITodoAPI';
import { Todo } from '../types'


interface TodoListProps {
  todoAPI: ITodoAPI;
}

const TodoList: React.FC<TodoListProps> = ({ todoAPI }) => {
  const [todos, setTodos] = useState<Todo[]>([]);

  useEffect(() => {
    const fetchTodos = async () => {
      const todos = await todoAPI.getTodos();
      setTodos(todos);
    };

    fetchTodos();
  }, [todoAPI]);

  return (
    <div>
      <h1>Todo List</h1>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <span>
              {todo.title}
            </span>
         </li>
        ))}
      </ul>
    </div>
  );
};

export default TodoList;

Enter fullscreen mode Exit fullscreen mode

With this, we can now test our component in isolation of the application, without any dependency on jest.mocks or an actual API running. For that we can use a Fake implementation of the API.

describe('TodoList', () => {
  // Fake implementation of the ITodoAPI
  const fakeTodoAPI: ITodoAPI = {
    getTodos: async (): Promise<Todo[]> => [
      { id: 1, title: 'Test Todo 1', completed: false },
      { id: 2, title: 'Test Todo 2', completed: true },
    ]
  };

  it('should render a list of todos', async () => {
    render(<TodoList todoAPI={fakeTodoAPI} />);

    // Wait for todos to be fetched and displayed
    await waitFor(() => screen.getByText('Test Todo 1'));

    expect(screen.getByText('Test Todo 1')).toBeInTheDocument();
    expect(screen.getByText('Test Todo 2')).toBeInTheDocument();
  });
});
Enter fullscreen mode Exit fullscreen mode

For using it on our application we can then on the root of the application pass the actual class for our component and it will work equally.

// src/App.tsx
import React from 'react';
import TodoList from './components/TodoList';
import { TodoAPI } from './api/TodoAPI';

const todoAPI = new TodoAPI();

function App() {
  return (
    <div className="App">
      <TodoList todoAPI={todoAPI} />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Conclusion

By using dependency injection, we can easily swap between fake and real implementations of the API in our React components. This makes the component modular, testable, and easier to maintain. Whether you are testing your component with a fake API in isolation or using the real API in production, the code structure remains the same, making it adaptable and scalable for larger applications.

Hopefully, it has helped! Happy coding.

Top comments (0)