If you've read any other pages on my blog you may know that an Application I work on uses AngularJS as the framework and ReactJS as a component library. So all of my page routes are defined in angular and then they resolve to a React Component. In order to take advantage of all of the AngularJS work we've already done, I use React2Angular to inject $injector as a prop into my parent container component. Up until this point, I then pass $injector down as props to children components. Here's an example:

Here's my main page definition. Pay attention to...

  • It defines a new module & state/route with AngularJS and maps it to a component which is a React2Angular component.
  • It imports a custom service and adds it as a requirement of my module so it so it's available from $injector.
  • It imports a ReactJS component fooContainer which is set as the source component in the react2angular function
  • It injects $injector as a prop to my component.
import { react2angular } from 'react2angular';
import FooContainer from './FooContainer/FooContainer.jsx';
const myCustomAPIService = require('../services/myCustomAPIService');
var angular = require('angular');
module.exports = angular.module('myApp.foo', [
myCustomAPIService,
])
.component('fooContainer', react2angular(FooContainer, [], ['$injector']))
.config(function($stateProvider) {
$stateProvider.state({
name: 'Foo',
url: '/episode-performance',
component: 'fooContainer'
});
})
.name;
view raw FooRoute.js hosted with ❤ by GitHub

Now let's look at that fooContainer.js.

import React, {Component} from 'react';
import PropTypes from 'prop-types';
let $injector; // Creates a top-scoped variable which will be set later.
class FooContainer extends Component {
constructor(props) {
super(props);
$injector = props.$injector; // Sets that top-scoped variable to the $injector passed through props.
}
render() {
return (
<div>
<h1>Foo Example Page</h1>
{/* Notice we are passing along $injector to a child component */}
<TestComponent $injector={$injector}/>
</div>
);
}
}
FooContainer.propTypes = {};
export default FooContainer;
view raw fooContainer.js hosted with ❤ by GitHub

And finally the child component which uses the $injector:

import React, {Component} from 'react';
let $injector;
class TestComponent extends Component {
constructor(props) {
super(props);
$injector = props.$injector;
this.myCustomAPIService = $injector.get('myCustomAPIService');
this.getData();
}
getData(){
this.myCustomAPIService.getCustomData('test').then((data) => {
console.log('data', data);
})
}
render() { return (<div> Hey </div>); }
}
export default TestComponent;

What I'm trying to avoid is having to explicitly pass down $injector through the entire tree. This isn't a big deal when the component using $injector is a direct descendant of the main component but what if 5 levels deep a component suddenly needs access to a service and needs $injector to get it? That means 4 other components will need $injector passed as props and pass them on to their children. This is dirty because if we refactor code we have to be aware of who needs $injector below it. Can we avoid this using the Context API? Let's try!

First I'm going to create a new ReactJS Context

import React from 'react'
const InjectorContext = React.createContext(null);
export default InjectorContext;

Next we're going use this Context in my main component file. Notice I'm no longer passing $injector in as a prop.

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import InjectorContext from './InjectorContext.js';
let $injector; // Creates a top-scoped variable which will be set later.
class FooContainer extends Component {
constructor(props) {
super(props);
$injector = props.$injector; // Sets that top-scoped variable to the $injector passed through props.
}
render() {
return (
<InjectorContext.Provider value={{$injector: $injector}}> {/* ### Here is our new context in use */}
<div>
<h1>Foo Example Page</h1>
{/* Notice we are NO LONGER passing along $injector to a child component */}
<TestComponent/>
</div>
</InjectorContext.Provider>
);
}
}
FooContainer.propTypes = {};
export default FooContainer;

Now we can get at that Context in our child component file but we have to do return a context-wrapped version of the component.

import React, {Component} from 'react';
let $injector;
class TestComponent extends Component {
constructor(props) {
super(props);
$injector = props.$injector;
this.myCustomAPIService = $injector.get('myCustomAPIService');
this.getData();
}
getData(){
this.myCustomAPIService.getCustomData('test').then((data) => {
console.log('data', data);
})
}
render() { return (<div> Hey </div>); }
}
// Rather than exporting the component directly, we're exporting our component wrapped in our
// Injector Consumer to get access to the context.
export default (props) => (
<InjectorContext.Consumer>
{context => <TestComponent {...props} $injector={context.$injector} />}
</InjectorContext.Consumer>
)

With this setup we should be able to copy paste the usage of this component any where in the descendant component tree without having to worry about who's passing it the $injector prop.

I would like to avoid having to export an extra functional component to wrap my component. Doing it this way adds extra bulk to the code and I lose the ability for my editor to use PropTypes and auto-complete like here:
Screen-Shot-2019-02-07-at-4.57.50-PM

To get around this limitation I have to define that functional component to a named variable and then add the PropTypes to that component like so, where AggregatePageHeaderContainerPre is the real component definition before having context added to it.
Screen-Shot-2019-02-07-at-4.59.46-PM

There should be away to do it cleaner using Class.contextType but for the life of me I can't get it to work. You can see in the example below I should be able to just define a contextType prop just like propTypes and it should apply the context and make it available as this.context in the life-cycle methods.
Screen-Shot-2019-02-07-at-4.47.57-PM

I've tried it a few times and every time this.context is never set to the context I define. 🤷‍♂️ Here's a working example of it but when I tried to incorporate this directly into my code this.context ends up being an empty object {}. So I dunno.