Dependency Injection in a redux action creator Dependency Injection in a redux action creator reactjs reactjs

Dependency Injection in a redux action creator


React-thunk supports passing an arbitrary object to a thunk using withExtraArgument. You can use this to dependency-inject a service object, e.g.:

const bluetoothService = require('./blueToothService');const services = {    bluetoothService: bluetoothService};let store = createStore(reducers, {},    applyMiddleware(thunk.withExtraArgument(services)));

Then the services are available to your thunk as a third argument:

function startDeviceScan() {    return function (dispatch, getstate, services) {        // ...        services.bluetoothService.startDeviceSearch((device) => {            dispatch(addDevice(device));        });    }}

This is not as formal as using a dependency-injection decorator in Angular2 or creating a separate Redux middleware layer to pass services to thunks---it's just an "anything object" which is kind of ugly---but on the other hand it's fairly simple to implement.


You can use a redux middleware that will respond to an async action. In this way you can inject whatever service or mock you need in a single place, and the app will be free of any api implementation details:

// bluetoothAPI Middlewareimport bluetoothService from 'bluetoothService';export const DEVICE_SCAN = Symbol('DEVICE_SCAN'); // the symbol marks an action as belonging to this api// actions creation helper for the middlewareconst createAction = (type, payload) => ({     type,    payload});// This is the export that will be used in the applyMiddleware methodexport default store => next => action => {    const blueToothAPI = action[DEVICE_SCAN];    if(blueToothAPI === undefined) {        return next(action);    }    const [ scanDeviceRequest, scanDeviceSuccess, scanDeviceFailure ] = blueToothAPI.actionTypes;    next(createAction(scanDeviceRequest)); // optional - use for waiting indication, such as spinner    return new Promise((resolve, reject) => // instead of promise you can do next(createAction(scanDeviceSuccess, device) in the success callback of the original method        bluetoothService.startDeviceSearch((device) => resolve(device), (error) = reject(error)) // I assume that you have a fail callback as well        .then((device) => next(createAction(scanDeviceSuccess, device))) // on success action dispatch        .catch((error) => next(createAction(scanDeviceFailure, error ))); // on error action dispatch};// Async Action Creatorexport const startDeviceScan = (actionTypes) => ({    [DEVICE_SCAN]: {        actionTypes    }});// ACTION_TYPESexport const SCAN_DEVICE_REQUEST = 'SCAN_DEVICE_REQUEST'; export const SCAN_DEVICE_SUCCESS = 'SCAN_DEVICE_SUCCESS'; export const SCAN_DEVICE_FAILURE = 'SCAN_DEVICE_FAILURE';// Action Creators - the actions will be created by the middleware, so no need for regular action creators// Applying the bluetoothAPI middleware to the storeimport { createStore, combineReducers, applyMiddleware } from 'redux'import bluetoothAPI from './bluetoothAPI';const store = createStore(  reducers,  applyMiddleware(bluetoothAPI););// Usageimport { SCAN_DEVICE_REQUEST, SCAN_DEVICE_SUCCESS, SCAN_DEVICE_FAILURE } from 'ACTION_TYPES';dispatch(startDeviceScan([SCAN_DEVICE_REQUEST, SCAN_DEVICE_SUCCESS, SCAN_DEVICE_FAILURE]));

You dispatch the startDeviceScan async action, with the action types that will be used in the creation of the relevant actions. The middleware identifies the action by the symbol DEVICE_SCAN. If the action doesn't contain the symbol, it dispatches it back to the store (next middleware / reducers).

If the symbol DEVICE_SCAN exists, the middleware extracts the action types, creates and dispatches a start action (for a loading spinner for example), makes the async request, and then creates and dispatches a success or failure action.

Also look at the real world redux middle example.


Can you wrap your action creators into their own service?

export function actionCreatorsService(bluetoothService) {   function addDevice(device) {      return { type: 'ADD_DEVICE', device }   }   function startDeviceScan() {      return function (dispatch) {         // The Service invokes the given callback for each found device         bluetoothService.startDeviceSearch((device) => {            dispatch(addDevice(device));         });      }   }   return {      addDevice,      startDeviceScan   };}

Now, any clients of this service will need to provide an instance of the bluetoothService. In your actual src code:

const bluetoothService = require('./actual/bluetooth/service');const actionCreators = require('./actionCreators')(bluetoothService);

And in your tests:

const mockBluetoothService = require('./mock/bluetooth/service');const actionCreators = require('./actionCreators')(mockBluetoothService);

If you don't want to specify the bluetooth service every time you need to import the action creators, within the action creators module you can have a normal export (that uses the actual bluetooth service) and a mock export (that uses a mock service). Then the calling code might look like this:

const actionCreators = require('./actionCreators').actionCreators;

And your test code might look like this:

const actionCreators = require('./actionCreators').mockActionCreators;