/reservice

An isomorphic/universal asynchronous tasks solution for redux.

Primary LanguageJavaScriptMIT LicenseMIT

reservice

An isomorphic/universal asynchronous tasks solution for redux.

npm version Build Status Test Coverage License

You may already using redux-thunk for your asynchronous tasks. Put thunk, function or promise into an action makes it not pure, which means the action may not be serialized or replayed well.

A better asynchronous task practice is: create your action as pure object, do asynchronous tasks in your own redux middlewares. This practice keep all your actions creators and reducers pure and clean, make your application more isomorphic or universal. The only place you put asynchronous codes are redux middlewares....Or, a better place: redux service (a.k.a. re-service).

A redux service means: an asynchronous task triggered by a start service action. After it done, the result will be dispatched as another service done action.

Re-service provides a good practice for all your asynchronous tasks, includes:

  • A helper function to create service action creator. (the action is FSA compliant)
  • A redux middleware to:
    • handle the service action
      • At client side, transport action to server then get result.
      • At server side, execute the service then get result.
    • then dispatch service result action
  • An express middleware to deal with transported service actions.

Install

npm install reservice --save

You will need these polyfills for older browsers or other environments:

Usage

A Service

// req is optional, you can receive req to deal with session based tasks.
const myService = (payload, req) => {
  // Do any async task you like, return a promise or result.
  // You can not know any redux related things,
  // but you can access the express request object here.
  ...
  return result;
}

A Service Action Creator

import { createService } from 'reservice';

// Check redux-actions to know more about payloadCreator
// https://github.com/acdlite/redux-actions#createactiontype-payloadcreator--identity-metacreator
const doSomeThing = createService('DO_SOMETHING', payloadCreator);

expect(doSomeThing('good')).toEqual({
  type: 'CALL_RESERVICE',
  payload: 'good',
  reservice: {
    name: 'DO_SOMETHING',
    state: 'CREATED',
  },
});

Define Service List

const serviceList = {
  [doSomeThing]: myService,
  [anotherServiceCreator]: theCodeOfAnotherService,
  ...
}

Setup Express Application

// your server.js
import { createMiddlewareByServiceList } from 'reservice';
import bodyParser from 'body-parser';

...

// reservice middleware need to access body as json
app.use(bodyParser.json());

// Add this line to declare serviceList and use the express middleware
app.use(createMiddlewareByServiceList(serviceList));

The Reducer

import { handleActions } from 'redux-actions';

// create a reducer
const myReducer = handleActions({
  [doSomeThing]: (state, action) => { ... },
  [anotherServiceCreator]: anotherReducer,
  ...
}, initialState);

// If you also want to take care service start, try this
import { ACTION_TYPE_RESERVICE } from 'reservice';

const nowLoadingReducer = (state = initialState, action) => {
  if (action.type !== ACTION_TYPE_RESERVICE) {
    return state;
  }

  // service started, remeber to set nowLoading to false in yourown reducers
  // you can set different loading states by checking action.reservice.name
  return { ...state, nowLoading: true };
}

Setup Redux Store

import { createStore, applyMiddleware } from 'redux';
import { serviceMiddleware, settleRequest } from 'reservice';

const store = createStore(
  myReducer,
  applyMiddleware(serviceMiddleware)
);

// Optional: If you like to access request in service
// You need to do this.
store.dispatch(settleRequest(req));

Example

Please check example to get a deeper guide.

Debug

  • reservice already adopt debug , you can export DEBUG=... to show debug log:
    • reservice:start : show service name, payload
    • reservice:receive : show service name, payload when client side dispatch received
    • reservice:success : show service name, payload and result when service successed
    • reservice:fail : show service name, payload and error when service failed
    • reservice:error : show service name, payload and error.stack when service failed
    • reservice:select : show service name, payload and selected result when service successed, refer to selector.

Optional Usage: Change Default Setting

You can change default service path and method by this way:

import { setupServiceEndpoint } from 'reservice';

setupServiceEndpoint('/mypath/my_service/');

// Or also change http method
setupServiceEndpoint('/another/path/ok', 'POST');

Optional Usage: createService

If you plan to migrate from redux-thunk and prefer to reuse your action type, you may specify the startType and endType when you createService.

const doSomeThing = createService({
  endType: 'DO_SOMETHING',
  startType: 'DO_SOMETHING_STARTED',
  payloadCreator,
  metaCreator,
});

store.dispatch(doSomeThing('good'));

expect(store.dispatch.calls.argsFor(0)).toEqual([{
  type: 'CALL_RESERVICE',
  payload: 'good',
  reservice: {
    name: 'DO_SOMETHING',
    start: 'DO_SOMETHING_STARTED',
    state: 'CREATED',
  },
}]);

// After the reservice middleware handled the action,
// another action will be dispatched.
expect(store.dispatch.calls.argsFor(1)).toEqual([{
  type: 'DO_SOMETHING_STARTED',
  payload: 'good',
  reservice: {
    name: 'DO_SOMETHING',
    start: 'DO_SOMETHING_STARTED',
    state: 'CREATED',
    ...
  },
}]);

// After the service is done, this action will be dispatched.
expect(store.dispatch.calls.argsFor(2)).toEqual([{
  type: 'DO_SOMETHING',
  payload: 'the result...',
  error: false,
  reservice: {
    name: 'DO_SOMETHING',
    start: 'DO_SOMETHING_STARTED',
    state: 'END',
    ...
  },
}]);

Optional Usage: Selector

In most case you may try to refine API response data by selector function then put it into redux store. You can do it inthe service, or do it in the reducer.

If you run selector in the service, you dispatch only selected data to reducer. This practice save time and space when the smaller service result be transmitted from server to client, but prevent you to see full API response in network or redux debugging tools.

If you run selector in the reducer, you dispatch full data to reducer. This practice may take more time and space to transmit result from server to client, but you can see full API response in debug tools.

Reservice provide two small functions to help you adopt all these two practices in development and production environments:

// The selector function
const mySelector = result => ({
  data: result.body.data.reduce(refineMyData, {}),
  error: result.body.error
});

// In a service
import { prodSelect } from 'reservice';
// Run your selector only when in production environment, keep original result when in development environment.
const myProdSelector = prodSelect(mySelector);
const myService = payload => callAPI(payload).then(result => myProdSelector(result));

// In a reducer
import { devSelect } from 'reservice';
// Run your selector only when in development environment, keep original result when in production environment.
const myDevSelector = devSelect(mySelector);
const myReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'MY_SERVICE':
      return { ...state, ...myDevSelector(action.payload) }
  }
  return state;
}

Advanced Usage: Selector

Or, you can define service as { service, selector } , reservice will keep full result in action.reservice.full_payload for debugging when not in production environment. And, the selected result still be placed in action.payload.

// Original Service code with result selector
const selectResult = (result) => result.body.items;
export myService = (payload, req) => callSomeApi({ ...payload, req }).then(selectResult);

// change export from function into { service , selector } for better debugging info
export myService = {
  service: (payload, req) => callSomeApi({ ...payload, req }),
  selector: (result) => result.body.items,
};

Here is a migration example to adopt reservice selector.