I remember the day when I “discovered” dependency injection. I was having a difficult time testing a console view, and all of my mocking strategies were failing to test a simple function.
sayHello() {
console.log("HELLO")
}
// Tests
it("says HELLO", () => {
// ¯\_(ツ)_/¯
});
As I spent the afternoon Googling and reading Sandi Metz, I stumbled upon dependency injection.
interface ILogger {
log(message: string): void;
}
sayHello(logger: ILogger) {
logger.log("HELLO")
}
// Tests
it("says HELLO", () => {
const mockLogger: ILogger = {
log: jest.fn()
}
sayHello(mockLogger);
expect(mockLogger.log).toHaveBeenCalledWith("HELLO");
});
When writing this test, I realized that I didn't really care that much about how my message gets logged, just as long as I know there is a logger receiving the correct message.
Sometimes we forget this concept when we are writing front-end code. In our views, we tend to become very dependent on concrete implementations. For example, let’s look at this page with an Alert component.
Pretty simple, right? I can click the button to show an alert, and if there is an alert in the state, my page displays a FancyAlertMessage
component.
Using Jest and Enzyme, I can pretty easily test that my component works.
But let’s say some time passes, and I need to re-visit the way my application handles alerts. Maybe there are some places where I want to show a FancyAlertMessage
component, and there are other places where I want to show an OrdinaryAlertMessage
. My class has a concrete dependency on FancyAlertMessage
, so if I want to re-use that logic, I either have to re-write the entire class or extract that dependency.
What is Dependency Inversion?
According to the definition of Dependency Inversion:
A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend on details. Details should depend on abstractions.
While it can sometimes be hard to wrap our minds around technical language like this, here's my best approximation of what this means for us.
-
As much as possible, the logic in our class should depend on abstractions. In this case, we will be abstracting the state management so we don't need to create this from scratch in every component that uses the same logic.
-
The abstraction we are using shouldn't tell me which component to render. For example, it shouldn't care whether the component is a
FancyAlertMessage
, or anOrdinaryAlertMessage
, and I shouldn't have to change the abstraction every time I add another type of alert view. Instead, it should handle only the implementation details I want to use globally, such as error logging or user-friendly wording of server errors.
Why not Dependency Injection?
If I wanted to take a cue from my previous discovery and use pure dependency injection, my first instinct might be to inject props from my parent class.
SimpleComponentWithAlert
is no longer responsible for its own state, but there are still a couple of problems with this. First of all, we are now requiring this component to be used with a parent that manages state, which is still coupling it to the state management logic. Secondly, the abstraction depends on FancyAlertMessage
, which could be considered a detail. Instead, let's use another form of dependency inversion that better accomplishes our goals. Rather than removing the concrete dependency on a specific alert component, we are going to de-couple the component only from the state management responsibilities.
What are Render Props?
In React, render props are the arguments that get passed to a component’s children. Basically the component is saying “I don’t care what this child is, but I know that it will take an alert
prop.” This is a great pattern for encapsulating state, and giving child components the ability to access and update it. You will sometimes see this pattern as a render
function prop on the parent component, but in this example we'll just be creating a function using the children
prop.
First, let’s extract the state and the showAlert
function to a class called AlertManager
. This is the logic that will be shared between multiple components.
Notice how this class isn’t rendering any specific content. Instead it is calling its children as a function with the props alert
and showAlert
. Now we have a lot less logic in SimpleComponentWithAlert
.
In this component, we are now using AlertManager
to keep track of the state and provide a way to update it—notice how the components in this class now have access to alert
and showAlert
as arguments. And after this refactor, the test we wrote above should still pass.
Tools like Redux are great for managing global state inside an application, but if we only need state shared between a small set of components, patterns like this one are often a better way to start. With render props, we can build and test our components in isolation, without mocking out global state or a Redux store.
This pattern is becoming more and more commonly used, and can be seen in libraries like Downshift, React Final Form, React Router and in the new React Context API. Whether you are a beginner or an expert, this pattern is a great one to recognize and understand.