Dependency Injection in React With Provider Pattern

Adam Klein

The Provider pattern introduces a way to access some global object (service) in deeply nested components. If you’ve been using React for a while, you probably used it already. In fact, many popular libraries use this pattern: react-router, react-redux, mobx-react, react-intl, react-dnd, or styled-components.

A provider that wraps a component tree:

<Provider>
  <Component/>
</Provider>

An injector as higher-order function:

export default withProvider(Component);

You can also relate to this pattern as dependency injection as a mechanism of exposing a global dependency to nested components on demand, somewhat similar to Angular’s mechanism, but not as a general definition of dependency injection in software development.

Let’s say we want to create a customizable theme for our app and use it in any component we’d like:

const SendButton = () => (
  <button style={{ color: theme.primaryColor }}>Send</button>
);

Somehow we need the theme instance to be available in this component. Moreover, we want to make it reactive, meaning whenever we change the theme, we want the component to update.

Let’s start by creating a top-level component that will hold the theme, and pass it down using React context:

export class ThemeProvider extends React.Component {
  state = {
    primaryColor: '#448866'
  };

  static childContextTypes = {...};

  getChildContext() {
    return {
      getTheme: () => this.state
    };
  }

  render() {
    return <span>{ this.props.children }</span>;
  }  
};

Next, we’ll put the provider in the top-level of our app, to make the theme available everywhere:

const App = () => (
  <ThemeProvider>
    ...
  </ThemeProvider>
);

Now we need a way to access the theme from child components. We could do it directly from the context, but there are a few drawbacks to this:

  • Using context directly is not the best practice;
  • We are coupling our components to ThemeProvider;
  • We can’t use React’s lifecycle hooks to react to changes in theme.

So instead we use an injector:

function withTheme(WrappedComponent) {
  const Wrapper = (props, { getTheme }) => (
    <WrappedComponent
      getTheme={ getTheme }
      { ...props }
    />
  );

  Wrapper.contextTypes = {...};

  return Wrapper;
}

The injector is a higher-order component (HOC) that takes the theme from the context and passes it down as props. Now to use the theme inside a component, you wrap it with the withTheme HOC:

const Component = (props) => (
  <div style={{ color: props.getTheme().primaryColor }}>
    This is the primary color
  </div>
);

export default withTheme(Component);

Reusing a service

Let’s now say we want to have two different themes side by side on our page. Since we are using context, we just need to put two ThemeProvider’s and each one of them will supply a different theme instance for its component subtree:

<div>
  Theme 1:
  <ThemeProvider>
     ... // components using theme 1
  </ThemeProvider>

  Theme 2:
  <ThemeProvider>
    ... // components using theme 2
  </ThemeProvider>
</div>

See full source code and demo on StackBlitz

When to use the pattern

Usually you can use generic store mechanisms like React and MobX to access global resources, but in some cases Redux and MobX are not good enough:

  • You are creating a generic component that should serve any architecture (for example if you create styled-components and want to supply a theme);
  • You need the services to be reusable on the page, and be able to declare a service per component tree;
  • You need to save large amounts of data that you don’t want on the store.

Other (bad) approaches

You could pass the theme down the component tree as a prop. In this case all the component’s ancestors have to be aware of the theme and pass it down which creates coupling, and is not a very productive way of writing code.

You could also export a theme object and import it wherever you need. In this case it won’t be reactive out of the box, meaning if we change the theme it won’t be reflected in the components that use it. Also, if we want to support showing two themes on the same page we can’t do that with a global singleton.

Popular