Dependency Injection in React (1)
Dependency injection is a simple idea:
Put depended instance objects in the constructor parameter list of a class, so that when constructing the class object, the dependency can be easily swapped for a different implementations.
The benefit of doing this is that we do not need to know the real implementation of the depended class when defining our class. The real depended class implementation will only be required during class instantiation, a.k.a runtime.
Maybe this is not obvious.
Let me show an example in React.
So in my User Filter component, I have this.
import { optionService } from '@services'
export const UserFilter = () => {
const options = useOptions(optionService.getOptions)
return <Select>
{options.map(o => <Option value={o} />)}
</Select>
}
The component depends on optionService
to get a list of options. The optionService
is closely coupled with business logics. So different use case will have different business logics. And we are importing it here by an import
statement. In other words, we have to define it somewhere in the code base and instantiate it as a singleton before using it here.
This is bad.
Especially when I need to extract the UserFilter out to a separate package.
How to fix this? Dependency Injection.
We can specify the optionService
as one of the required props. Because we know the exact behavior of optionService
, we can define an interface for it too. So anyone who use this shared component knows how to pass in the required service.
import { IOptionService } from '@types'
export const UserFilter = ({ optionService: IOptionService }) => {
const options = useOptions(optionService.getOptions)
return <Select>
{options.map(o => <Option value={o} />)}
</Select>
}
As simple as this, we have removed the previous requirement of having to instantiate a singleton service before using it.
As the typescript is doing structural typing, there is no need to share the IOptionService
as well. As long as the user knows the shape of it, it will work.