In my last post, we looked at Aurelia’s dependency injection library and how you might be able to use it as a standalone library in your vanilla JavaScript application. Now, however, I want to take a look at how you might be able to wire it into your React application, and then finally how you might hook it into React/Redux application.
The React App
My React app is built to do the same thing as the vanilla JavaScript application with four different components that are designed to show several injection scenarios.
One of the problems that you have to conquer when hooking Aurelia into a React application is that React is responsible for calling the constructor of your components. If you want to get your dependencies injected, then you need to do it in a way that plays nicely with React, and this is a perfect use case for a higher-order-component.
If you are not familiar with the concept of higher order functions, then I would suggest that you read through a few blog posts on functional programming to get the hang of it, but essentially we are going to be creating a function that wraps our original function (the component) to add functionality.
Injection Higher Order Component
I am not a big fan of using React’s context to pass information down to child components so I would rather pass the injected types into my components via their props. To do that I need to create a higher order component that is aware of the current container and the required dependencies so that it can wrap the current component and pass those in as props.
I want to try to future proof this code so that it will hopefully work with the decorator spec once it is finalized, so I am going to create a function that takes in the options and returns another function that takes in the target function (component).
export function configureInject(options, types) { if (!types) { types = []; } return function(target) {...}; };
Inside of that second function, we need to create a React component that renders the target component but modifies the props that are passed into the target component so that we can inject the required types.
import React, { createElement } from 'react'; import hoistStatics from 'hoist-non-react-statics'; return function(target) { class InjectedComponent extends React.Component { constructor(props) { super(props); } render() { return createElement(target, this.props); } }; InjectedComponent.wrappedComponent = target; return hoistStatics(InjectedComponent, target); };
This higher order component simply wraps another component and renders it without modifications right now, but once we have a reference to our container and the required types, then we can mess with the props that are passed into the wrapped component to actually inject them.
One other piece that is really important any time that you create a higher-order-component is the piece that hoists the statics from the target component to the higher order component. This would allow any static functions or properties defined on the target component to be called from the higher order component.
Now that we have our component wrapped, let’s make the final changes to resolve the dependencies.
import React, { createElement } from 'react'; import hoistStatics from 'hoist-non-react-statics'; import container from './rootContainer'; export function configureInject(options, types) { if (!types) { types = []; } return function(target) { const targetName = target.displayName || target.name || 'Component'; class InjectedComponent extends React.Component { constructor(props) { super(props); if (options.useChildContainer) { const containerToUse = props.container || container; this.container = containerToUse.createChild(); } else { this.container = props.container || container; } } render() { const injectedTypes = types.map((type) => this.container.get(type)); const modifiedProps = Object.assign({}, this.props, { container: this.container, injections: injectedTypes }); return createElement(target, modifiedProps); } } InjectedComponent.wrappedComponent = target; InjectedComponent.displayName = `InjectedComponent(${targetName})`; return hoistStatics(InjectedComponent, target); }; }
That code should allow you to take a normal React component and define an array of its dependencies and then have them injected as props.
Wiring up the HOC
Using this higher order component is very simple. All you need to do is wrap your component with it before exporting, and then you can pull your dependencies off of the props.
import React from 'react'; import MyService from '../services/MyService'; import { configureInject } from '../di/containerHelpers'; class MyComponent extend React.Component { constructor(props) { super(props); this.service = props.injections[0]; } ... }; export default configureInject({}, [ MyService ])(MyComponent);
There are some things that you might be able to do to make this more robust or performant so that it is not resolving against the container in every render, but the general approach in this example should be solid. One other thing that you could do would be instead of defining an array of dependencies, define key/value pairs instead so that you could inject them directly onto prop keys instead of having an injections array. My preference is to namespace them on the injections key to prevent possible prop naming collisions, though.
Injecting React Components
If you want to be able to inject React components as dependencies to other components then you need to be careful how you do it. If you remember, the container normally constructs the types when resolving them, but that does not work with React because React needs to be the one to construct the components. As a result, if you want to inject components themselves then the constructor functions need to be registered as an instance so that the container will return the function untouched and let React new it up.
There is one additional thing that you have to watch out for when injecting React components. If you are using JSX then your injected component variable needs to start with an uppercase letter or else React will treat it as an element.
const MyComponent = container.get(ComponentA); const myComponent = container.get(ComponentA); return ( <div> <MyComponent>This works</MyComponent> <myComponent>This does not</myComponent> </div> );
The Rest of the App
Since this is just a React app without any state management libraries the rest of your app is most likely going to be vanilla JS (or close to it) and you would not have to do anything special to wire those pieces up with your DI container.
Redux
A React example is nice to see, but the hottest part of the React ecosystem right now is to use Redux for managing your state. While a Redux application is going to look pretty similar to a React app in some regards, specifically with the higher order components, Redux also adds a few other layers that need to be accounted for. If you are not familiar with Redux, then I would strongly suggest reading through their docs first.
Containers
A Redux container, not to be confused with Aurelia’s container, is a component that is connected to the Redux store. When you connect a component to a store you grab state values from your store and pass them as props to the component, and you also pass in references to action creators as dispatch props to the component. Any of our non-connected components can still use the same injection higher-order-component from the React example, but the connected components will need a slightly different approach so that we can inject the bound action creators as well as the state selector functions.
React-Redux already supplies a higher order component called “connect” that wraps the component and modifies the props with the state and action creators. Ideally, this approach would just encapsulate the connect logic so that we do not have to rewrite the store subscription itself.
If you truly want to fully decouple your container from your other layers, then you need to be able to inject the state selector functions, action creators, and any other arbitrary item needed by your component.
Action Creators
In addition to needing to be injected into the container’s mapDispatchToProps function, the action creators themselves may need to have services injected into them. Fortunately, when the container resolves the action creator it can also resolve the dependencies of the action creator, so our solution will need to provide a higher order function that wraps the action creators and defines their dependencies.
Selectors
The selectors are supposed to be slim and operate directly against the state object so we should not need to worry about injecting any dependencies into them.
My Approach
My approach involved creating wrappers for all three prop handlers that are passed to the connect higher order component: stateProps, dispatchProps, and mergeProps. These three handlers are responsible for ensuring that the correct DI container is used for that specific connected component and resolving all of the required dependencies.
mapStateToProps
If you want to inject selector(s) into your mapStateToProps function, then you need to define a function that takes in the state, ownProps, and the injected selector(s) as arguments. When you wrap that with the higher order function then at runtime the selectors will be injected so that they can return your state props.
const mapStateToProps = (state, ownProps, injectedUserSelector) => { const user = injectedUserSelector(state); return { firstName: user.firstName, lastName: user.lastName, email: user.email }; }; const injectedStateToProps = injectSelectors([USER_SELECTOR_ID])(mapStateToProps);
mapDispatchToProps
mapDispatchToProps is a little easier since it really only returns action creators. In this case, you can just return an object of key/value pairs where we will resolve the values against the container and use those resolved action creators instead.
import { fetchUserProfile } from '../actionCreators/user'; const mapDispatchToProps = { fetchUserProfile: fetchUserProfile };
Action Creators
Action creators, other than fulfilling a part of the Redux design pattern, are vanilla JavaScript functions and that means that they can have their own dependencies injected the same way that things are injected in vanilla JS apps.
import UserService from '../services/UserService'; import { inject } from 'aurelia-dependency-injection'; export function fetchUserProfile(userService) { return function() { return (dispatch) => { dispatch({ type: 'FETCH_USER' }); return userService.loadUserProfile().then((data) => { dispatch({ type: 'USER_RESPONSE', firstName: data.firstName, lastName: data.lastName, email: data.email }); }); }; } } inject(UserService)(fetchUserProfile);
As long as your action creators are registered as transients, then the container will construct new instances with up to date dependencies every time. You could probably also get away with doing a singleton per container instance if you are worried about the extra overhead of using transients.
Implementation
The last piece to look at is our final implementation.
import React, { createElement } from 'react'; import hoistStatics from 'hoist-non-react-statics'; import container, { retrieveContainerId, setIdOnContainer } from './rootContainer'; import UserService from '../services/UserService'; import { connect, bindActionCreators } from 'react-redux'; const DISPATCH = Symbol('Dispatch'); const INJECTED_SELECTORS = Symbol('InjectedSelector'); const CONTAINER = Symbol('Container'); /* This will determine if a container instance was passed in or if it needs to create one. This will also set a unique id on the new container and register services on it. */ function determineContainer(stateProps, ownProps, options, registrationFn) { // if a container has already been set on the state props then use that if (stateProps && stateProps[CONTAINER]) { return stateProps[CONTAINER]; } let currentContainer = container; if (ownProps && ownProps.container) { currentContainer = ownProps.container; } else if (stateProps && stateProps.container) { currentContainer = stateProps.container; } if (options && options.useChildContainer) { const childContainer = currentContainer.createChild(); setIdOnContainer(childContainer); registerServices(childContainer, registrationFn); return childContainer; } return currentContainer; } function registerServices(containerToUse, registrationFn) { // allow the redux container to register services on the container if (typeof registrationFn === 'function') { registrationFn(containerToUse); } } /* This creates a decorator function that will allow you to inject reselect selectors into your mapStateToProps function */ export function injectSelectors(types) { return function(target) { // return a function that takes the container so we can resolve the types before calling the real state to props function function mapStateToPropsWrapper(container) { const injectedSelectors = types.map((type) => container.get(type)); return function injectedStateToProps(state, ownProps) { return target(state, ownProps, ...injectedSelectors); }; }; mapStateToPropsWrapper[INJECTED_SELECTORS] = true; return mapStateToPropsWrapper; }; } // this is meant to be used with a Redux container export function injectConnect(options, types, registrationFn, mapStateToProps, mapDispatchToProps) { return function(target) { // we don't want to bind in the dispatch props function, since we need to inject the action creators later // use this to grab a reference to the dispatch function function dispatchProps(dispatch, ownProps) { const dispatchedProps = Object.assign({}, mapDispatchToProps); dispatchedProps[DISPATCH] = dispatch; return dispatchedProps; } // create a wrapper for the state props that determines if we need to inject a container or not function stateToProps(state, ownProps) { // we need to set the container on the state so that mergeProps can use the same container instance const containerToUse = determineContainer(state, ownProps, options, registrationFn); if (typeof mapStateToProps === 'function') { if (mapStateToProps[INJECTED_SELECTORS]) { const injectedStateToProps = mapStateToProps(containerToUse); return Object.assign({}, { [CONTAINER]: containerToUse }, injectedStateToProps(state, ownProps)); } else { return Object.assign({}, { [CONTAINER]: containerToUse }, mapStateToProps(state, ownProps)); } } return Object.assign({}, { [CONTAINER]: containerToUse }, mapStateToProps); } // handle the dispatch props and merge the state/own/dispatch props together function mergeProps(stateProps, dispatchProps, ownProps) { const containerToUse = determineContainer(stateProps, ownProps, options, registrationFn); const injectedTypes = types.map((type) => containerToUse.get(type)); // resolve the action creators from the container and bind them to dispatch // grab the reference to the dispatch function passed along const dispatch = dispatchProps[DISPATCH]; const boundDispatchProps = {}; Object.keys(dispatchProps).forEach((key) => { if (key === DISPATCH) return; const actionCreator = containerToUse.get(dispatchProps[key]); boundDispatchProps[key] = (...args) => { dispatch(actionCreator.apply(null, args)); }; }); // add the injections and the container to the props passed to the component return Object.assign({}, ownProps, stateProps, boundDispatchProps, { injections: injectedTypes, container: containerToUse }); } return connect(stateToProps, dispatchProps, mergeProps)(target); } }
That is a fair amount of code, but the result is that the boilerplate code in your containers ends up being pretty minimal.
import { injectConnect, injectSelectors } from '../di/containerHelpers'; import UserProfileComponent from '../components/UserProfileComponent.jsx'; import ComplicatedService from '../services/ComplicatedService'; import { fetchUserProfile } from '../actionCreators/user'; import { USER_SELECTOR_ID } from '../reducers/user'; const mapStateToProps = (state, ownProps, injectedUserSelector) => { const user = injectedUserSelector(state); return { firstName: user.firstName, lastName: user.lastName, email: user.email }; }; const injectedStateToProps = injectSelectors([USER_SELECTOR_ID])(mapStateToProps); const mapDispatchToProps = { fetchUserProfile: fetchUserProfile }; export default injectConnect({}, [ ComplicatedService ], null, injectedStateToProps, mapDispatchToProps)(UserProfileComponent);
Instead of using the connect higher order component to wrap your container, now you use the injectConnect and pass it your options, required types, and the mapStateToProps and mapDispatchToProps functions. The other major difference is that there is one extra line to register the selectors that need to be injected into the mapStateToProps function.
Conclusion
While all of that code works and properly injects everything into the different layers, I am not entirely sure that the added complexity of debugging your app would make it worth it in the long run. When you get a bug report the easy part would be figuring out which component has the issue, but then you would also need to know which DI container resolved that component, and which selectors and action creators were injected. I think after working through this that I would probably switch to a simpler solution for the Redux example and just use a single container or even just use the simple higher order component from the React example and forget about injecting selectors and action creators. I hope though that these two articles have helped give you some ideas of how you might be able to leverage the Aurelia dependency injection library in your code.
Leave a Comment