Do we really need dependency injection in Javascript?

Sergey Radzishevskii
6 min readFeb 3, 2020

Come on, it is a frontend, we never used Dependency Injection

We are used to using DI in the backend. It has complex logic, the dependency tree is large, it’s likely that we can change some Logger / Caching / HttpClient library to another. What about the frontend… do we really need this?

DI decouples our code, making it easier to modify and understand. But what if my code is simple enough and I don’t need to modify it.

// api.jsexport default {
saveOrder(orderData) {
const url = `${process.env.API_URL}/orders`;
// do POST request to URL
}
}
// OrderForm.tsx
import api from "path/to/api.js"
function OrderForm() {
submitOrder(orderData) {
api.submitOrder(orderData)
}
return (<form onSubmit={submitOrder} /> )
}

I know that I will never replace MyRestApi. In most cases that’s all I need, let’s be pragmatic - why should I care about dependency injection?

What about tests

Hmm … that’s a good question. One good reason to use injections is testing. If we use DI, we can easily replace the original dependency with a mocked version and easily create our unit test. Fortunately, jest provides us with great mocking functionality.

├── src
│ ├── __mocks__
│ │ └── api.js
│ └── api.js
// src/__mocks__/api.jsexport default {
saveOrder(orderData) {
return { id: 123, ... }
}
}

Now babel-jest will replace the original api.js during compilation with a mocked version. And we can easily test our target component. Only jest can do that trick. But we are not going to use another tool for testing.

Wait a second… let’s think about what just happened - we replaced the original dependency api.js with a mocked version. We override and inject the correct version of our dependency. So it’s dependency injection that has been done with babel-jest magic.

class A {
constructor(httpClient: HttpClientInterface) {
this.httpClient = httpClient;
}
doStuff() {
this.httpClient.get();
}
}
// vs// if we can replace it during import it's even simpler :)
import httpClient from "/path/to/axiosInstance.js";
function doStuff() {
httpClient.get();
}

Not a big difference… The exception is - we cannot easily replace httpClientimplementation. For example, if we want to change axios to fetch. But it’s unlikely that we will change libraries.

We are still in good shape. At this point, there’s no need to use a common DI approach with constructors. Let’s think about more complex scenarios.

Nested dependencies

Well, that was a simple example, now let’s look at classes that have nested dependencies.

class RestApi {
construct(httpClient: AxiosInstance) {}
}
class OrderManager {
constructor(api: RestApi) {}
}

In that case, we will have hardcoded dependencies.

import OrderManager from "path/to/OrderManager.js"const orderManager = new OrderManager(
new RestApi(
axios.instance({})
)
)

Let’s think about how we can improve this. What if we return an object instead of a class.

// RestApi.js
import axiosInstance from "path/to/axiosInstance.js"
class RestApi {
construct(httpClient: AxiosInstance) {}
}
export default new RestApi(axiosInstance);
// OrderManager.js
import restApi from "/path/to/RestApi.js"
class OrderManager {
constructor(api: RestApi) {}
}
export default new OrderManager(restApi);

// finally we can do
import orderManager from "path/to/OrderManager.js"
orderManager.saveOrder(...)

A slight flaw — we will create a new instance on each import. But in Javascript, creating objects is a cheap operation, so it’s not a game-changer. And it will not have a significant impact on application performance.
It is also possible that we want to initiate classes with a slightly different configuration.

class Logger() {
construct(logLevel: integer) {}
}
const errorLogger = new Logger(LoggerLevel::ERROR);
const infoLogger = new Logger(LoggerLevel::INFO);

But this is a rare case, in most cases, we need one object per one class. In the worst case, we can split it again into multiple files.

// logger.js
export default class Logger{}
// infoLogger.js
import Logger from "path/to/Logger.js"
export default new Logger(LoggerLevel::INFO);
// errorLogger.js
import Logger from "path/to/Logger.js"
export default new Logger(LoggerLevel::ERROR);

Now we see that dependency injection is not a requirement we can easily keep our code without it. And what about DI containers?

Maybe there are some crucial benefits in DI containers?

DI container configures our dependencies and allows us to keep configuration in one place.

const orderManager = new OrderManager(
new RestApi(
axios.instance({})
)
)
// in DIC you will have"httpClient": axios.instance({}),
"restApi": object(RestApi)
.construct(get("httpClient")),
"orderManager": object(OrderManager)
.construct(get('restApi')

DI configuration looks more complicated. DIC could be useful on the backend but we don’t have too many dependencies on the frontend. Thus, we can easily keep our frontend app without DIC.

A time to cast away stones, and a time to gather stones together

Well, it looks like we can easily keep our frontend applications without dependency injections and dependency injection containers.

Now please click Ctrl+F and look for the word “but”.

  • But what if my code is simple enough and I don’t need to modify it.
  • But we are not going to use another tool for testing.
  • But it’s unlikely that we will change libraries.
  • But in Javascript, creating objects is a cheap operation, so it’s not a game-changer.
  • But this is a rare case, in most cases, we need one object per one class.
  • But we don’t have too many dependencies on the frontend

All of these assumptions are incorrect!

Let’s be fair — some of these “but”-s are still rare cases.

What’s the price of using DI and DI containers?

We need to add a new package. A few more Kbs to your code is not a big deal nowadays. And you need time to learn how to use it. As a rule, all DIC libraries are quite easy to learn and use.

Pros and Cons

Don’t use DI containers

  • Don’t spend time learning a new package
  • Explicit code is more clear while it’s small. To be fair: it depends on many factors and very subjective. The bound between small and simple class and god class is very thin.

Use DI containers

  • Code is decoupled
  • Code is flexible
  • Configuration in one place
  • You operate with contracts/interfaces (see soliD)*
  • Your resources are singletons

The Dependency Inversion Principle

Finally, I want to talk a little bit more about the last D letter in SOLID abbreviation. The Dependency Inversion Principle says:

Entities must depend on abstractions not on concretions. It states that the high level module must not depend on the low level module, but they should depend on abstractions

Here’s an example,

class UsersManager {
constructor(...otherDependenciess, logger LoggerInterface) {
....
this.logger = logger;
}

register(user: UserData) {
try {
// @TODO
// create user in DB
// send confirmation email
// etc
} catch (e) {
logger.error(e);
}
}
}

I am creating some user manager class. It has a logger dependency to log some information and error messages. But in fact, I do not care about the implementation of the logger, while I create it, it is not so important at this point. I focus on business logic that allows us to register a new user. I create unit tests mocking LoggerInterface before I even have LoggerInterface implementation. It helps me think differently. Create important high-level modules first. Design from top to bottom, not vice versa. Don’t make it depend on loggers or any other low-level modules.

And yes we can do that exercise with jest. But let’s be fair nobody does it. If you use DI container, you work with interfaces/contracts naturally and follow the Dependency Inversion principle.

Moreover, when you have an easy way to inject dependencies and deliver them in place it’s much easier to break up big classes into smaller ones that would follow the Single Responsibility Principle.

Conclusion

We can live without DI containers, many projects do this. But when using DI containers, you can break the blockers and make your code decoupled and much flexible.

Want to start using DI container — click the link.

--

--

Sergey Radzishevskii

Enjoy turning complex systems into intelligible architectures chilling in my garden.