loading...
Cover image for When and Why you should do Dependency Injection in React

When and Why you should do Dependency Injection in React

shadid12 profile image Shadid Haque ・5 min read

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 ***

Discussion

pic
Editor guide
Collapse
sarneeh profile image
Jakub Sarnowski

I have few remarks to your article:

The typical module system in JavaScript has a caching mechanism.

Typical module system in NodeJS, not JavaScript. This doesn't work that way on the browser.

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.

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?

Collapse
shadid12 profile image
Shadid Haque Author

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 :)

Collapse
sjbuysse profile image
sjbuysse

Would love to hear about the caveats & how to address them :)