Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dependency Injection in a redux action creator

I'm currently building a learner React/Redux Application and I can not wrap my head around how to do dependency injection for services.

To be more specific: I have a BluetoothService (which abstracts a 3rd Party Library) to scan for and connect to other devices via bluetooth. This service gets utilized by the action creators, something like this:

deviceActionCreators.js:

const bluetoothService = require('./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));
      });
   }
}
module.exports = { addDevice, startDeviceScan };

(I am using the thunk-middleware)

My Problem however is: how to inject the service itself into the action-creator?

I don't want that hard-coded require (or importin ES6) as I don't think this is a good pattern - besides making testing so much harder. I also want to be able to use a mock-service while testing the app on my work station (which doesn't have bluetooth) - so depending on the environment i want another service with the same interface injected inside my action-creator. This is simply not possible with using a static import.

I already tried making the bluetoothService a parameter for the Method itself (startDeviceScan(bluetoothService){}) - effectively making the method itself pure - but that just moves the problem to the containers using the action. Every container would have to know about the service then and get an implementation of it injected (for example via props). Plus when I want to use the action from within another action I end up with the same problem again.

The Goal: I want to decide on bootstrapping time which implemenation to use in my app. Is there a good way or best practice for doing this?

like image 981
David Losert Avatar asked Apr 02 '16 15:04

David Losert


4 Answers

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.

like image 55
mikebridge Avatar answered Oct 20 '22 01:10

mikebridge


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 Middleware
import 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 middleware
const createAction = (type, payload) => ({ 
    type,
    payload
});

// This is the export that will be used in the applyMiddleware method
export 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 Creator
export const startDeviceScan = (actionTypes) => ({
    [DEVICE_SCAN]: {
        actionTypes
    }
});

// ACTION_TYPES
export 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 store
import { createStore, combineReducers, applyMiddleware } from 'redux'
import bluetoothAPI from './bluetoothAPI';

const store = createStore(
  reducers,
  applyMiddleware(bluetoothAPI);
);

// Usage
import { 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.

like image 44
Ori Drori Avatar answered Oct 20 '22 02:10

Ori Drori


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;
like image 2
Calvin Belden Avatar answered Oct 20 '22 03:10

Calvin Belden


I created a dependency-injecting middleware called redux-bubble-di for exactly that purpose. It can be used to inject an arbitrary number of dependencies into action creators.

You can install it by npm install --save redux-bubble-di or download it.

Your example using redux-bubble-di would look like this:

//import { DiContainer } from "bubble-di";
const { DiContainer } = require("bubble-di");
//import { createStore, applyMiddleware } from "redux";
const { createStore, applyMiddleware } = require("redux");
//import reduxBubbleDi from "redux-bubble-di";
const reduxBubbleDi = require("redux-bubble-di").default;

const bluetoothService = require('./blueToothService');

DiContainer.setContainer(new DiContainer());
DiContainer.getContainer().registerInstance("bluetoothService", bluetoothService);

const store = createStore(
    state => state,
    undefined,
    applyMiddleware(reduxBubbleDi(DiContainer.getContainer())),
);

const startDeviceScan = {
    bubble: (dispatch, bluetoothService) => {
        bluetoothService.startDeviceSearch((device) => {
            dispatch(addDevice(device));
        });
    },
    dependencies: ["bluetoothService"],
};

// ...

store.dispatch(startDeviceScan);
like image 2
Ben Avatar answered Oct 20 '22 03:10

Ben