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:
ThemeProvider
;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);
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
Usually you can use generic store mechanisms like Redux and MobX to access global resources, but in some cases Redux and MobX are not good enough:
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.
Consider signing up for React Clinic, an opportunity for you to get free advice, insights, and opinions from expert React developers. We're happy to meet for one hour to review your code or theoretical questions.