React Has Built-In Dependency Injection

François Zaninotto
François ZaninottoMarch 13, 2019
#react#tutorial#popular

Dependency Injection is a popular pattern that many frameworks implement - but React.js, apparently, doesn't. It turns out React has a dependency injection system built in JSX, and you probably already use it.

What is Dependency Injection?

Dependency Injection solves a common problem, which is hardcoded dependencies. When an object A depends on a second object B and creates that second object, then the dependency cannot be changed.

For instance, this Calculator class create its own logger service, which cannot be anything else than ConsoleLogger:

import ConsoleLogger from "./ConsoleLogger";

class Calculator {
  constructor() {
    this.logger = new ConsoleLogger();
  }

  add(a, b) {
    this.logger.log(`Adding ${a} to ${b}`);
    return a + b;
  }
}

But when testing this class, developers may want to mock the logger, to avoid polluting the console while testing. Or, in production, they may want to use a Syslog logger, or a logger sending logs to a SaaS service. All that isn't possible, since the dependency is hardcoded.

The Dependency Injection technique solves that problem by replacing new statements with parameters.

-import ConsoleLogger from './ConsoleLogger';

class Calculator {
-   constructor() {
-       this.logger = new ConsoleLogger();
-   }
+   constructor(logger) {
+       this.logger = logger;
+   }

    add(a, b) {
        this.logger.log(`Adding ${a} to ${b}`);
        return a+b;
    }
}

Now it's not up to object A to create object B, but to the caller of object A:

const logger = new ConsoleLogger();
const calculator = new Calculator(logger);
const result = calculator.add(1, 2); // console shows "Adding 1 to 2"

And developers can replace the dependency by another one:

const logger = new NullLogger();
const calculator = new Calculator(logger);
const result = calculator.add(1, 2); // console shows nothing

Dependency injection is a form of inversion of control. It goes beyond constructor injection (parameter injection, setter injection, etc), but you get the idea. Dependency Injection increases the modularity of the code, and it's considered a good practice.

It's often cumbersome to keep track of object instances (like the logger above), and to instanciate them only on demand. That's why, when you want to apply the dependency injection pattern, you need to use a dependency injection container. It's a library that takes care of keeping dependencies in a registry, instanciating them on demand, and making them configurable.

Angular offers a dependency injection container. Symfony offers another one. Spring has dependency injection built in. But React.js doesn't. How come, since dependency injection is such a good practice?

JSX Is Dependency Injection

React offers dependency injection without needing a dependency injection container, thanks to JSX.

Take a look at the following list of product reviews rendered in a table, taken from the react-admin demo:

Reviews as Datagrid

Here is the React code that renders the table:

const ReviewList = props => (
  <List resource="reviews" perPage={50} {...props}>
    <Datagrid rowClick="edit">
      <DateField source="date" />
      <CustomerField source="customer_id" />
      <ProductField source="product_id" />
      <RatingField source="rating" />
      <TextField source="body" label="Comment" />
      <StatusField source="status" />
    </Datagrid>
  </List>
);

The <List> component fetches the "/reviews" route in a REST api and passes the perPage parameter. But <List> doesn't display the list of reviews. Instead, it delegates the rendering to its child component, <Datagrid>, which displays the list as a table. That means that <List> depends on <Datagrid> for the rendering. And it's the caller of <List> (in the previous example, <ReviewList>) that sets this dependency.

So the parent-child relationship, in this example, is a form of dependency injection.

And just like for other forms of dependency injection, I can change the dependency very easily. If I want the reviews list to be displayed as a simple list instead of a datagrid, I just need to replace the child of <List> by another component, as follows:

import { List, Datagrid, TextField, DateField } from 'react-admin';

const PostList = props => (
    <List resource="posts" perPage={50}>
        <SimpleList
            primaryText={review => <ItemTitle record={review} />}
            secondaryText={review => review.body}
        >
    </List>
)

Reviews as SimpleList

Dependency Injection on Steroids

React even allows to inject more than one dependency to an element. First, because an element can have more than one child. In the previous example, <Datagrid> receives the list of columns that it should display:

const ReviewList = props => (
  <List resource="reviews" perPage={50}>
    <Datagrid rowClick="edit">
      <DateField source="date" />
      <CustomerField source="customer_id" />
      <ProductField source="product_id" />
      <RatingField source="rating" />
      <TextField source="body" label="Comment" />
      <StatusField source="status" />
    </Datagrid>
  </List>
);

So I can use the child relationship to pass a list of dependencies. Second, it's possible to inject dependencies using props. In the next example, I override the datagrid body with my own component:

const ReviewList = props => (
    <List resource="reviews" perPage={50}>
-       <Datagrid rowClick="edit">
+       <Datagrid rowClick="edit" body={<MyDatagridBody />} >
            <DateField source="date" />
            <CustomerField source="customer_id" />
            <ProductField source="product_id" />
            <RatingField source="rating" />
            <TextField source="body" label="Comment"/>
            <StatusField source="status" />
        </Datagrid>
    </List>
)

So any prop is a good way to pass a dependency or a list of dependencies. The children prop is a bit special, since it accepts natively a list of elements, and also because it is very expressive (as you can easily visualize dependencies using the tree structure).

Oh, and since dependencies can be component themselves, I can pass parameters to each dependency using their props:

const ReviewList = props => (
    <List resource="reviews" perPage={50}>
        <Datagrid
            rowClick="edit"
-           body={<MyDatagridBody />}
+           body={<MyDatagridBody withBulkActions />}
        >
            <DateField source="date" />
            <CustomerField source="customer_id" />
            <ProductField source="product_id" />
            <RatingField source="rating" />
            <TextField source="body" label="Comment"/>
            <StatusField source="status" />
        </Datagrid>
    </List>
)

So JSX offers the basic features of a dependency injection container: transitive dependencies, and configurability. And the Node.js module system lets me load components just like if I was using a service registry.

Service Locator

I've illustrated dependencies in templating, but dependency injection is often used to inject services, like a logger or a translator. React encourages using components for services, too.

For instance, I can pass a translator service to a component using its props:

const englishTranslator = message => {
  if (message == "hello.world") {
    return "Hello, World!";
  }
  return "Not yet translated";
};

const Greeting = ({ translate }) => {
  return <div>{translate("hello.world")}.</div>;
};

const App = () => <Greeting translate={englishTranslator} />;

However, for such services, injecting the service to all the components that need using props it is cumbersome. So React provides a way to require a dependency defined earlier using the context:

import React, { useContext } from "react";

const englishMessages = message => {
  if (message == "hello.world") {
    return "Hello, World!";
  }
  return "Not yet translated";
};

const TranslationContext = React.createContext();

const Greeting = () => {
  const translate = useContext(TranslationContext);
  return <div>{translate("hello.world")}.</div>;
};

const App = () => (
  <TranslationContext.Provider value={englishMessages}>
    <Greeting />
  </TranslationContext.Provider>
);

So the context provides some kind of Service Locator, which is an alternative to Dependency Injection Containers. Using contexts make components a bit harder to test. React warns against abusing contexts - they should only be used to share data or services across the entire application, such as for translation or theming.

Interface Segregation

One thing that React is missing is the ability for a component to set an interface for its dependencies. JavaScript is a loosely typed language, so type checking isn't a common practice.

Fortunately, TypeScript is changing that, and it's possible for a component to specify the props that a child component must accept.

For instance, the <List> component can enforce the type of child component it supports by listing the props this child will receive:

interface ListChildProps {
    data: any;
    ids: Identifier[];
    perPage?: number;
}

interface ListProps {
    children: React.ReactElement<ListChildProps>;
}

const List: React.SFC<ListProps> = props => (
    ...
)

Now, let's try to use this <List> component with a child that doesn't accept these props:

interface DummyProps {
  foo: string;
}
const DummyListView: React.SFC<DummyProps> = () => <span>Hello, dummy!</span>;

const ReviewList = props => (
  <List resource="reviews" perPage={50}>
    <DummyListView />
  </List>
);

Does TypeScript raise an error at compile time? Unfortunately, at the time of writing, no. Don't ask me why - deep type checking is an ongoing effort in TypeScript. See the discussion ongoing in issue #13618.

Conclusion

Why doesn't React ship a Dependency Injection Container like Angular.js? Because it doesn't need it. JSX and contexts are good enough to make React applications modular and testable.

That's also what I like about React: even for large applications or libraries, React does the job without requiring large frameworks.

For a deep dive into the dependency injection and inversion of control patterns, I recommend reading Inversion of Control Containers and the Dependency Injection pattern by Martin Fowler.

If you like React, make sure you give react-admin a try. It's a framework we built to accelerate the development of B2B apps with React.js. React-admin is open-source, documented, maintained, and already used by thousands of developers across the globe.

If you're curious, check out these demo apps that were built using react-admin:

e-commerce demo

Did you like this article? Share it!