Skip to main

Dependency Injection in React Native using Typescript + Inversify + RxJS

SOLIDify your system architecture

The patterns and approaches discussed in this post are fairly advanced, so I tried my best to be as thorough and clear as possible. Please let me know if you have any thoughts on how to improve it!

Motivation

It is of the utmost importance that we plan out our app’s system architecture as early as possible, preferably from the very beginning. However, getting to start a project from a blank slate is a rare “luxury”. More often we end up on a project that has already spent some time in development, and very often that project is a mess. No tests, no coherent organization, everything tightly coupled together – does this sound familiar? But even when we do get a chance to build a project from the ground up, it doesn’t always turn out the way we want it to. Many times we rush to deliver at least something, make an MVP or a POC, and then build on that. During that initial rushed stage, we don’t have time to consider anything other than just “making it work”. What’s our architecture going to be like? Well, basically, we make it up as we go. After a while, we can no longer allow a project to simply grow “naturally”. We need to ensure that our codebase is maintainable, our test coverage is extensive and our architecture enables us to deliver new features and fixes quickly. The further development progresses, the more often the question comes up – “How do we manage all this complexity?” There’s no ultimate answer to this question, no one-size-fits-all solution. The good news is that there are many excellent tools that help a great deal with it. Unfortunately, too many developers don’t know about them and try to reinvent the wheel. When it comes to React/React-Native apps in particular, I’ve seen Redux used as the go-to solution for managing higher-level business logic far too many times. (No, this isn’t what Redux is for.) Even if you’re the smartest person on Earth, using other people’s knowledge is always a good idea. Humanity got this far only because we shared information between us and built on top of each other’s discoveries and inventions. Reading about a pattern after I’ve organically arrived at it on my own, without knowing “that this thing I’ve been doing actually has a name”, always feels frustrating to me. Just how much time and effort could I have saved if I’d known about it sooner and didn’t have to invent it myself? Never step on the same rake as other people. Learn from their mistakes and successes. So let’s tap into the endless ocean of our collective knowledge and experience. What’s in there? Best practices, design patterns, development processes – a whole slew of tools for us to learn and use. For complex systems, I strongly recommend Dependency Injection. Why? Here are some of its benefits:

  • It helps with managing instantiation and caching
  • It significantly simplifies testing. TDD becomes extremely easy.
  • It enables a truly modular and decoupled architecture
  • It provides an easy way to access singletons or shared instances
  • It provides an easy way to override and extend existing configurations

In short, DI is great. OOP is great. SOLID is great. Those are facts, proven by decades of software. If you’re unfamiliar with these terms, it’s very important that you make an effort to learn and truly understand them before implementing the pattern described in this post. I recommend Robert C. Martin’s Clean Code as one of the best books on using SOLID in real life, and Object-Oriented Thought Process is a great read as well. The video below is, in my opinion, one of the best tutorials on DI out there, so make sure to check it out.

A good understanding of these concepts really matters here. So without any further ado, I’ll go through one of the most effective and reliable approaches to organizing React Native app architecture. I’ve been using it in many of my projects, and it has been working out incredibly well.

What about the web?

Right off the bat, I’d like to note that in my opinion this pattern shouldn’t be used for most web apps. Even though Inversify is a relatively lightweight library – around 4KB – on the web every byte matters. reflect-metadata can also bloat the bundle size significantly. I strongly recommend avoiding RxJS on the web as well, unless your app is as complex as Figma or Clubhouse.io, since the package weighs 45.6KB bundled. I’ll go into detail on how this approach can be implemented without RxJS in the closing parts of this post. But size isn’t the only concern here. This approach creates an overhead that should be avoided on web clients at all costs. The ideal state for a web application is a statically served page, so our architecture should gravitate towards using as little Javascript as possible. After all, we do have a Javascript problem on the web. This is a completely different platform, with completely different requirements, and we should take those into consideration. The less code we write on the front-end, and the simpler and “dumber” it is, the better. If you’re using something as sophisticated as the DI pattern for a simple blog, you’re doing something wrong. I learned this the hard way while creating this website.

The Basics

Let’s begin. What’s Inversify? From their website:

InversifyJs is a powerful and lightweight inversion of control container for JavaScript & Node.js apps powered by TypeScript.

This tool helps you write clean and efficient OO code that adheres to all the SOLID principles, by making it possible to implement a proper Dependency Injection pattern. Here’s how it works.


import { Container, injectable, inject } from "inversify";

@injectable()
class Katana {
  public hit() {
      return "cut!";
  }
}

@injectable()
class Shuriken {
  public throw() {
      return "hit!";
  }
}

@injectable()
class Ninja implements Warrior {

  private _katana: Katana;
  private _shuriken: Shuriken;

  public constructor(katana: Katana, shuriken: Shuriken) {
      this._katana = katana;
      this._shuriken = shuriken;
  }

  public fight() { return this._katana.hit(); };
  public sneak() { return this._shuriken.throw(); };

}

var container = new Container();
container.bind<Ninja>(Ninja).to(Ninja);
container.bind<Katana>(Katana).to(Katana);
container.bind<Shuriken>(Shuriken).to(Shuriken);

Pretty neat, eh? But how do we make it work with React? Usually a container takes care of creating object instances and provides all of the dependencies via a constructor. This is where things get complicated, since in React we don’t have easy access to (class components’) constructors. How do we bind our components to a container? There are a couple of tricks we can use. One working solution, for example, is @lazyInject decorators. The idea is that since object creation has already been taken care of, dependencies are injected at the moment they’re referenced instead of at instantiation time. Here’s a great post by Tomasz Świstak explaining this and a couple of other approaches. You might notice that the post is a little outdated, though. Many of the libraries it references haven’t been maintained for years and probably no longer work, and with the advent of hooks you rarely see class components these days. But don’t worry, making it work with hooks is just as easy. Here’s another post by Tomasz where he goes over how to inject services into functional components using hooks. Here’s the code for the most important part, modified just a tiny little bit to fit my personal use.


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 ServiceProvider: 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('No context container!');
}
return container.get<T>(identifier);
}

The idea is pretty straightforward – we store our DI container in React Context and create a convenient hook that first retrieves the container from context, then retrieves the dependency from the container. Here’s what it looks like in action. Let’s say we have a service that provides a certain kind of data. In our case, let it be the name of the user. Here it is:


interface INameService {
getName: () => string
}

@injectable()
class NameService implements INameService {

  private _name: string;

  public getName() { return this._name; };
}

const NameServiceId = Symbol('NameService');

We bind this service to our container.


Container.bind<INameService>(NameServiceId).to(NameService);

And then use it in our React components.


import React from 'react';
import { useInjection } from './ioc.react';
import { INameService, NameServiceId } from './services';

export const Hello: React.FC = () => {
  const service = useInjection<INameService>(NameServiceId);

  return (
      <h1>Hello {service.getName()}!</h1>
  );
};

This looks pretty good already, but we aren’t quite there yet. Let’s imagine the name value isn’t immediately available and we need to fetch it from some backend API first. Our NameService would then look something like this:


@injectable()
class NameService implements INameService {

  private _name: string = '';

  public getName() { return this._name; };

  public fetchName() {
    fetchNameFromAPI().then((name) => {
      this._name = name;
    });
  };
}

And in our Hello component we could fetch the name on the first mount like this:


export const Hello: React.FC = () => {
  const service = useInjection<INameService>(NameServiceId);

	useEffect(() => {
		service.fetchName();
	}, []);

  return (
      <h1>Hello {service.getName()}!</h1>
  );
};

You can probably already see the problem. Even if we successfully retrieve the name from the API and store it in the service property, the component won’t re-render, because neither state nor props are being updated. The approach we’ve been using so far is imperative. How do we make it reactive? My solution is to use RxJS. I’ll go into the details of this in a bit, but for now I’d like to address a few things. You might think, “Isn’t this a little bit of an overkill?” Well, yeah, it is. But you aren’t limited to using RxJS strictly for the functionality described in this post. It’s an extremely powerful and useful library that you can put to full use anywhere throughout your app. If anything, using RxJS with DI will push you towards using it in other parts of your app, which in turn can also significantly improve your app’s maintainability and performance. But then again, it isn’t an absolute requirement.

So, how does it work? First, turn the class properties our UI state depends on into BehaviorSubjects. Then, instead of simply assigning a new value to a property, we call next(). Here’s what that looks like:


@injectable()
class NameService implements INameService {

  private _name: BehaviorSubject<string> = new BehaviorSubject<string>('');

  public getName() { return this._name; };

  public fetchName() {
    fetchNameFromAPI().then((name) => {
      this._name.next(name);
    });
  };
}

You might notice that getName no longer returns a string but a BehaviorSubject<string> instead. This is fine, because we won’t be accessing it in our component directly. In fact, we’ll abstract all service-related logic away into something called a facade. I picked up this idea, along with a couple of other RxJS tricks, from this post by Thomas Burleson. We’ll use this little onEmit helper method to make our life a bit easier.


export function onEmit<T>(source: Observable<T>, nextFn: (value: T) => void): Subscription {
return source.subscribe({
  next: nextFn,
  error: (err) => console.error(err),
});
}

Then we’ll create a facade that only exposes the name string and the fetchName function, like this.


const useNameProvider = () => {
const service = useRef(useInjection<INameService>(NameServiceId));

const [name, setName] = useState('');

const fetchName = () => {
  return service.current.fetchName();
};

useEffect(() => {
  const subscriptions: Subscription[] = [
    onEmit<string>(service.current.getName(), (value) => {
      setName(value);
    }),
  ];
  return () => {
    subscriptions.forEach((it) => it.unsubscribe());
  };
}, []);

return {name, fetchName};
};

Ok, there’s a lot going on here. First, you might notice that we store the service instance in a ref using the useRef hook. Why is that? Because if the service isn’t bound to the container as a singleton, the container creates a new instance of it on every container.get() call. This means that if we don’t store it as a ref, we’ll get a completely new instance of NameService on each render, with an empty _name string. Singletons, obviously, aren’t affected by this, since only one instance is created and then reused on all consecutive container.get() calls. Next, what exactly is going on in useEffect? Basically, once the component mounts we subscribe to any _name updates. Once a new value is fired a callback is invoked, which in our current case will update the state of the component this facade hook is called in. This way, once we trigger an update on all _name observers using _name.next(value), the state of this component will be updated with the new value. We obviously delete the subscription once the component unmounts. We also bind the fetchName method by wrapping it with an arrow function to avoid any scoping issues. This is why we do:


const fetchName = () => {
  return service.current.fetchName();
};
...
return {name, fetchName};

Instead of simply:


// Don't do this
return {name, fetchName: service.current.fetchName};

And here’s what our Hello component ends up looking like once we use this facade.


export const Hello: React.FC = () => {
  const {name, fetchName} = useNameProvider();

	useEffect(() => {
		fetchName();
	}, []);

  return (
      <h1>Hello {name}!</h1>
  );
};

And there you have it. A clean, simple and easy-to-read component that is actually running a powerful dependency injection setup under the hood, with the business logic separated from the view. Make sure to read part 2 ✌️ (once it’s ready), where I go through a real-life example of using this pattern.

Caveats

  • Boilerplate – as you’ve probably already noticed, this approach requires writing a lot of very explicit, boilerplate code. When it comes to productivity and code analysis tools, we JS/TS developers are at a relative disadvantage. C# developers, for example, have incredibly powerful tools such as Visual Studio and ReSharper that make managing very verbose and complex code easy. We have nothing that comes even close. Perhaps creating custom emmet autocompletions could help alleviate this problem.
  • Decorators are still an experimental syntax – hopefully one day they’ll be included in the standard. But what if the proposal gets rejected? Of course, it won’t be the end of the world, but project maintenance would become much more difficult. It’s already pretty tricky to set up an unorthodox bundler configuration and make @babel/plugin-proposal-decorators work correctly. If the proposal were rejected, setting up projects with decorators using newer technologies would only get harder and harder.
  • Hard to grasp for newcomers – many React Native developers come from a web background, and many web developers, even senior ones, aren’t very familiar with OOP and SOLID.
  • Seriously, it’s hard to grasp for newcomers – RxJS is a huge library that requires a very thorough study of its concepts before you can use it. What is a pipe operator? What’s the difference between a subject and a behavior subject? You either know the meaning of these terms or you don’t.
  • I’m going to repeat myself, but I think it’s important enough to stress: don’t use this approach on the web unless you absolutely have to.

Closing remarks

As I mentioned before, RxJS isn’t necessary to make this work. Let’s get back to our first example. We could refactor the service’s fetchName method to return the new name value:


public async fetchName () {
return fetchNameFromAPI()
}

And then we could refactor the fetchName function inside the useNameProvider facade to set the state like this:


const useNameProvider = () => {
const service = useRef(useInjection<INameService>(NameServiceId));

const [name, setName] = useState('');

const fetchName = async () => {
	const newName = service.current.fetchName();
  setName(newName);
};

return {name, fetchName};
};

And if you need something more sophisticated and flexible, the onEmit+Subscription setup could also be replaced with an array of callbacks stored inside the service.

And that’s pretty much all there is to it. Stay tuned for 🚀part 2🚀, where I’ll go over the real-life use of this pattern. I hope you found this post useful. If you ever decide to try this approach in your own projects, please let me know how it worked out! And again, feel free to reach out if you have any thoughts or questions. As always, thanks for reading! 🥳

This website uses cookies to persist user choice made on this banner... And that's about it. This website respects your privacy and doesn't store any third-party cookies. All collected analytics data is anonymous. By continuing to use this website, you consent to the use of cookies.