Our React applications are composed of many small components or modules. The components that we write will sometimes be dependent on each other. As our application grows in size a proper management of these dependencies among components becomes necessary. Dependency Injection is a popular pattern that is used to solve this problem.
In this article we will discuss
- When is it necessary to apply dependency injection pattern
- Dependency injection with Higher Order Components (HOC)
- Dependency injection with React Context
Note: If you have no prior knowledge of dependency injection, I would recommend the following blog post
Let’s consider the following example.
// app.js
function App() {
const [webSocketService, setwebSocketServicet] = React.useState({});
React.useEffect(() => {
// initialize service
setwebSocketServicet({
user: `some user`,
apiKey: `some string`,
doStuff: () => console.log("doing some function")
});
}, []);
return (
<div>
<B socket={webSocketService} />
</div>
);
}
Here we have our App
component that is initializing a service, and passing the reference as a props to its children.
// B.js
function B(props) {
return (
<div>
<A {...props} />
</div>
);
}
// A.js
function A(props) {
// Do something with web socket
const doWebSocket = () => {
props.socket.doStuff();
};
return (
<div>
<button onClick={() => doWebSocket()}>Click me</button>
{props.children}
</div>
);
}
Component B
receives the props from App
and passes it down to A
. B
doesn't do anything with the passed props. Our websocket
instance should somehow reach the A
component where it is being used. This is a very basic example application but in a real world scenario when we have lots of components nested inside one another we have to pass this property down all the way. For instance
<ExampleComponentA someProp={someProp}>
<X someProp={someProp}>
<Y someProp={someProp}>
//.... more nesting
//... finally Z will use that prop
<Z someProp={someProp} />
</Y>
</X>
</ExampleComponentA>
Lots of these components are acting as proxy in passing this prop to their children. This is also making our code less testable, because when we write tests for these components (X or Y) we have to mock someProp
even though the only purpose of that property is to pass it down the children tree.
Now let's see how we can solve this problem with a Dependency Injection using a Higher Order Component.
Let’s create a file called deps.js
and inside the file we will have two functions
import React from "react";
let dependencies = {};
export function register(key, dependency) {
dependencies[key] = dependency;
}
export function fetch(key) {
if (dependencies[key]) return dependencies[key];
console.log(`"${key} is not registered as dependency.`);
}
Here in the dependencies
object we will store names and values of all our dependencies. The register
function simply registers a dependency and fetch
function fetches a dependency given a key.
Now we are going to create a HOC that returns a composed component with our injected properties.
export function wire(Component, deps, mapper) {
return class Injector extends React.Component {
constructor(props) {
super(props);
this._resolvedDependencies = mapper(...deps.map(fetch));
}
render() {
return (
<Component
{...this.state}
{...this.props}
{...this._resolvedDependencies}
/>
);
}
};
}
In our wire
function we pass a Component
, an array of dependencies
and a mapper
object and it returns a new Injected
component with the dependencies as props. We are looking for the dependencies and mapping them in our constructor. We can also do this in a lifecycle
hook but for now let’s stick with constructor for simplicity.
Alright, let’s go back to our first example. We will be making the following changes to our App
component
+ import { register } from "./dep";
function App() {
const [webSocketService, setwebSocketServicet] = React.useState(null);
React.useEffect(() => {
setwebSocketServicet({
user: `some user`,
apiKey: `some string`,
doStuff: () => console.log("doing some function")
});
}, [webSocketService]);
+ if(webSocketService) {
+ register("socket", webSocketService);
+ return <B />;
+ } else {
+ return <div>Loading...</div>;
+ }
}
We initialized our WebSocket service and registered it with the register
function. Now in our A
component we do the following changes to wire it up.
+const GenericA = props => {
+ return (
+ <button onClick={() => console.log("---->>", +props.socket.doStuff())}>
+ Push me
+ </button>
+ );
+};
+const A = wire(GenericA, ["socket"], socket => ({ socket }));
That's it. Now we don't have to worry about proxy passing. There’s also another added benefit of doing all this. The typical module system in JavaScript has a caching mechanism.
Modules are cached after the first time they are loaded. This means (among other things) that every call to require('foo') will get exactly the same object returned, if it would resolve to the same file.
Multiple calls to require('foo') may not cause the module code to be executed multiple times. This is an important feature. With it, "partially done" objects can be returned, thus allowing transitive dependencies to be loaded even when they would cause cycles.
***taken from node.js documentation
What this means is that we can initialize our dependencies and it will be cached and we can inject it in multiple places without loading it again. We are creating a Singleton when we are exporting this module.
But this is 2019 and we want to use context api right ? Alright, so let’s take a look how we can do a dependency injection with React Context.
Note: If you would like to know more about how other SOLID principles apply to React. Check out my previous post here
Let’s create a file called context.js
import { createContext } from "react";
const Context = createContext({});
export const Provider = Context.Provider;
export const Consumer = Context.Consumer;
Now in our App
component instead of using the register function we can use a Context Provider. So let’s make the changes
+import { Provider } from './context';
function App() {
const [webSocketService, setwebSocketServicet] = React.useState(null);
React.useEffect(() => {
setwebSocketServicet({
user: `some user`,
apiKey: `some string`,
doStuff: () => console.log("doing some function")
});
}, []);
if (webSocketService) {
+ const context = { socket: webSocketService };
return (
+ <Provider value={ context }>
<B />
+ </Provider>
)
} else {
return <div>Loading...</div>;
}
}
And now in our A
component instead of wiring up a HOC we just use a Context Consumer.
function A(props) {
return (
<Consumer>
{({ socket }) => (
<button onClick={() => console.log(socket.doStuff())}>Click me</button>
)}
</Consumer>
);
}
There you go and that's how we do dependency injection with React Context.
Final Thoughts
Dependency Injection is being used by many React libraries. React Router and Redux are the notable ones. DI is a tough problem in the JavaScript world. Learning about these techniques not only makes one a better JavaScript developer but also makes us critically think about our choices while building large applications. I hope you liked this article. Please follow me and spare some likes ;)
Until next time.
*** NOTE: This post is a work in progress, I am continuously updating the content. So any feedback you can provide would be much appreciated ***
Top comments (5)
I have few remarks to your article:
Typical module system in NodeJS, not JavaScript. This doesn't work that way on the browser.
Even in NodeJS, this is not a best practice, as it can be easily broken. Check out this article.
And one more: I think it's not a good idea to use the Context API for injecting things like a websocket service. Context API is for passing
data
through the component tree, I guess that passing complicated objects out there might create problems (i.e. performance ones). For things like this - why not just import it?Hey Jakub, that is a great feedback. Thank you for the input. I will do some more research on the module system. I have been injecting services like simple websockets and firebase real time DB instances through Context. And I totally agree if not careful these will lead to memory leaks. I will do some more research on these as well and definitely address these caveats. Good feedback though. Loved it :)
Would love to hear about the caveats & how to address them :)
I would agree with Jakub comment that the Context API is better used for passing data. Also, have you consider using custom hook for reuse those thing?
This post is now quit old and yes custom hooks are a straight forward way to do this.