Explore how to streamline dependency injection in React by combining InversifyJS with Hooks, enabling flexible, efficient use in functional components.
Nearly a year ago I wrote an article about dependency injection in React. I showed how to inject dependencies into class components from InversifyJS containers. Since then we’ve witnessed the release of one of the most anticipated React features – Hooks. If you’ve never heard about them, you should definitely go to docs on React's website and learn about them. The key thing is that we can now do a lot more with functional components and they are really user-friendly. In this article, I’d like to show you how easy it is to use InversifyJS with Hooks with a very simple example.
The project we’ll be working on is nearly the same as the one from the previous article. The only difference is that I’ve refactored the Hello component to be functional. Everything’s done in TypeScript, but you can do it almost the same way in pure JavaScript (there are some differences in decorators usage, but these are covered in Inversify’s docs).
// Hello.tsx
import React from 'react';
import { useInjection } from './ioc.react';
import { IProvider } from './providers';
export const Hello: React.FC = () => {
const provider: IProvider<string>; // here we need to inject our provider
return (
<h1>Hello {provider.provide()}!</h1>
);
};
// index.tsx
import 'reflect-metadata';
import React from 'react';
import ReactDOM from 'react-dom';
import { Hello } from './Hello';
import { container } from './ioc';
const App: React.FC = () => {
return (
<div>
<Hello />
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
// ioc.ts
import { Container } from 'inversify';
import { IProvider, NameProvider } from './providers';
export const container = new Container();
container.bind<IProvider<string>>('nameProvider').to(NameProvider);
// providers.ts
import { injectable } from 'inversify';
export interface IProvider<T> {
provide(): T;
}
@injectable()
export class NameProvider implements IProvider<string> {
provide() {
return 'World';
}
}
Listing 1 Example project without connection between Inversify and React
For more information about that code, I recommend reading the “Example project” section of a previous article.
To accomplish this task, we will mainly use two React functionalities – Context API (since v.16.3) and React Hooks (since v.16.8).
Context API is the way to pass data through the whole component tree without using props. You can read about the details in docs, since I don’t want to go deep into the weeds of Context here. You’ve probably used it already in an indirect way – a lot of popular libraries use it (react-redux, styled-components, etc.). If a library tells you to wrap everything on the root level with a provider component, then it probably uses Context.
Our injection provider will comprise two elements:
We will use Context to create a provider component. First, we need to create a new Context with the createContext function. This returns an object containing two ready components – Provider and Consumer. Provider is a top-level component which is used to send things down into React’s tree, and Consumer uses that data. What does this mean for us? That we can use it directly to accomplish our task. An example of how this can be done is in Listing 2.
// ioc.react.tsx
import React from 'react';
import { Container } from 'inversify';
const InversifyContext = React.createContext<{ container: Container | null }>({ container: null });
type Props = {
container: Container;
};
export const Provider: React.FC<Props> = (props) => {
return (
<InversifyContext.Provider value={{ container: props.container }}>
{props.children}
</InversifyContext.Provider>
);
};
Listing 2 Injection provider component
Pretty simple, isn’t it? If you remember inversify-react or react-inversify from the previous article, you will surely know how to use it. It’s exactly the same component, but this time it’s been made by us. Listing 3 shows us how to use it in the code on the application’s root level.
// index.tsx
import 'reflect-metadata';
import React from 'react';
import ReactDOM from 'react-dom';
import { Hello } from './Hello';
import { container } from './ioc';
import { Provider } from './ioc.react';
const App: React.FC = () => {
return (
<Provider container={container}>
<div>
<Hello />
</div>
</Provider>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
Listing 3 Usage of Provider component
Now we need a way to get the object from Inversify’s container. We want to do it as a function which will work as a React Hook, and it should use the container provided by the user in the Provider component. So, how are we to achieve this?
As you likely remember, after creating Context we had an object which contained a Provider and Consumer. Usually, the Consumer is used as a component to access the data in a context through it. However, since we have React Hooks, we don’t need to use a Consumer. One of the most important Hooks built into React is useContext. It does a simple thing – it returns the current context value and subscribes to its changes. In Context we have the container instance, so we can use any method from it that we need (in our case the method will be get, but you may also want to create an additional hook with getAll to simulate multiInject). Of course, the developer should provide the identifier to our Hook as an argument so that Inversify will be able to identify what we need. You can see a very simple implementation of this in Listing 4.
// ioc.react.tsx
import React, { useContext } from 'react';
import { Container, interfaces } from 'inversify';
const InversifyContext = React.createContext<{ container: Container | null }>({ container: null });
type Props = {
container: Container;
};
export const Provider: React.FC<Props> = (props) => {
return (
<InversifyContext.Provider value={{ container: props.container }}>
{props.children}
</InversifyContext.Provider>
);
};
export function useInjection<T>(identifier: interfaces.ServiceIdentifier<T>) {
const { container } = useContext(InversifyContext);
if (!container) { throw new Error(); }
return container.get<T>(identifier);
};
Listing 4 Whole implementation of injection provider and hook
Now we can safely use it in our example app, in Hello.tsx, as shown on Listing 5.
// Hello.tsx
import React from 'react';
import { useInjection } from './ioc.react';
import { IProvider } from './providers';
export const Hello: React.FC = () => {
const provider = useInjection<IProvider<string>>('nameProvider');
return (
<h1>Hello {provider.provide()}!</h1>
);
};
Listing 5 Hello.tsx with useInjection hook
As you can see, in a few lines of code we were able to create the whole provider for the dependency injection with very simple usage. Of course, it can be extended – you may want to add a hook for multi-injection or a higher order component to use in class components.
You can find the whole example on my GitHub.
Since it’s so simple, I’m sure there are a lot of questions about it. I’ve thought about a few cases and want to give you answers in the same article.
That’s to keep a single source of truth and not to make the component dependent on a specific container. In one code base we may have many containers (e.g. each route has a different one), but components may be shared between them.
Of course not! It’s just to show an alternative way of achieving the same thing. It’s so simple that you may want to consider using it, but I’m not forcing you to do anything.
Yes, there are. While I was writing this article, I wanted to check whether someone had already implemented the idea. I found a very nice library – react-injection. It gives us both a higher-order component and a hook to provide injections into components. If you want to use a library to connect Inversify with React using hooks, this may be a good way to go.
This post was also published on ITNEXT.
If you want to read more tech tutorials and information, check out my previous articles!