Dependency Injection in React with useContext + TypeScript
TL;DR
In React and similar frameworks, you sometimes want to do DI-like things.
Specifically, this comes up when mocking SDKs or when implementing in a DDD-like fashion.
In most cases, such Client, Repository, or Service objects don't hold state that triggers React re-renders. So sharing them globally with useContext is unlikely to cause major issues.
With this mindset, the idea of using useContext for DI is fairly common:
- Using useContext for DI-like Patterns in React
- Using React Context as a DI Container
- React Context for Dependency Injection Not State Management
The Initialization Problem
Let's define an arbitrary interface and its implementation.
If you initialize it naively, it looks like this. However, this isn't ideal when using useContext as a DI container. We want to inject something that implements the interface from the outside, but a specific concrete instance is already injected from the start, creating a dependency on a particular implementation.
The practical downsides are the dependency on a concrete implementation and the fact that you don't need to provide an initial value to the Provider:
Initializing with undefined
As mentioned above, since we don't want to hold any concrete instance as the initial value, we initialize with undefined.
This removes the dependency on a concrete implementation, but introduces two new problems: skipping initialization doesn't cause an error, and the return value of useContext becomes SomeInterface | undefined, requiring an undefined check every time.
To solve this, I referenced the following article:
By using this function, the return value of useCtx becomes T, and failing to initialize the Provider results in an error. Here's how to use it:
Summary
By initializing the Context with undefined, making it throw an error when DI is not performed, and using a custom hook whose return value doesn't include undefined, you can achieve type-safe DI-like behavior.