redux-reducer-actions

1.0.0 • Public • Published

redux-reducer-actions

Redux-reducer-actions is a Redux store enhancer which allows action generation in reducer. It is quite useful to concentrate most of logic in reducer rather than split it to effects. You can use this library in your project to avoid verbose plumber code and make it clean and testable.

Pavel Lasarev paullasarev@gmail.com

Content

  • redux flow
  • redux-saga effects
  • redux-saga drawbacks
    • plumber code
    • unit testing is awful
  • example task: load extra info from API
    • redux-saga vs redux-reducer-actions
  • redux-reducer-actions
    • reducer actions
    • attach to store
    • options
    • useful redux libs

Redux flow

+------------+       +--------------+        +------------+
|            |       |              |        |            |
|  action    +------>+    reducer   +------->+   store    |
|            |       |              |        |            |
+-----^------+       +--------------+        +------+-----+
      |                                             |
      |                                             |
      |                                             |
      |                                             |
      |              +--------------+               |
      |              |              |               |
      +--------------+    UI        <---------------+
                     |              |
                     +--------------+

Redux-saga

redux-saga is a library that aims to make application side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) easier to manage, more efficient to execute, easy to test, and better at handling failures.

The mental model is that a saga is like a separate thread in your application that's solely responsible for side effects. redux-saga is a redux middleware, which means this thread can be started, paused and cancelled from the main application with normal redux actions, it has access to the full redux application state and it can dispatch redux actions as well

redux-saga flow

+------------+       +--------------+
|            |       |              |
|  action    +------>+    reducer   +---------+           +-----+
|            |       |              |         |     +---->+     |
+-----^------+       +--------------+         |     |     +-----+
      |                                +------v-----+----+
      |                                |                 |    +------+
      |                                | saga middleware +---->      |
      |                                |                 |    +------+
+-----+------+        +------------+   +------+-----+----+
|            |        |            |          |     |    +-------+
|    UI      <--------+   store    <----------+     +---->       |
|            |        |            |                     +-------+
+------------+        +------------+

redux-reducer-actions

redux-reducer-actions is an Redux store enhancer which allows action generation in reducer. That approach allows to concentrate most logic in one place - reducer.

Due to that fact that reducers are pure functions, it is extremely easy to test this logic and keep code clean and concise.

Actions, processed by redux-reducer-actions wouldn't go into the store. Rather they will be dispatched in the next event loop throw the standard Redux flow.

redux-reducer-actions flow

                                    +-------------------------------------------------------+
                                    |                                                       |
                                    |                                                       |
                                    |                                                       |
+---------------+            +------v-------+                     newState: {               |
|               |            |              |                        ...,                   |
|   action      +------------>   reducer    +-----------------+      actions: [actions],    |
|               |            |              |                 |   }                         |
+-------^-------+            +------^-------+                 |                             |
        |                           |                         |                             |
        |                           |             +-----------v-----------+                 |
        |                           |             |                       |                 |
        |                           |             |                       |                 |
        |                           |             |    redux              |        +--------+---------+
        |                           |             |    reducer            |        |                  |
        |                           |             |    actions            |        | dispatch         |
        |                           |             |                       +--------> actions          |
        |                           |             |    enhancer           |        | in new event loop|
        |                           |             |                       |        |                  |
        |                           |             |                       |        +------------------+
        |                           |             |                       |
        |                           |             +-----------+-----------+
        |                           |                         |
        |                           |                         |
+-------+---------+         +-------+-------+                 |    newState: {
|                 |         |               |                 |       ...,
|    UI           <---------+   store       +<----------------+    }
|                 |         |               |
+-----------------+         +---------------+

example task: load extra info from API

Redux code (common)

// action creators
export const GET_ITEM = 'GET_ITEM';
export const GET_ITEM_SUCCESS = GET_ITEM_SUCCESS';

export const getItem = (id) => ({
  type: GET_ITEM,
  request: {
     url: `/api/item/${id}`,
     method: 'GET',
     meta: { id },
  },
})

export const GET_FILE = 'GET_FILE';
export const GET_FILE_SUCCESS = 'GET_FILE_SUCCESS';

export const getFile = (id) => ({
  type: GET_FILE,
  request: {
     url: `/api/file/${id}`,
     method: 'GET',
     meta: { id },
  },
})

redux-saga code

// reducer
export const reducer(state, action) {
  switch(action.type) {
     case GET_ITEM_SUCCESS: {
       return {
         ..state,
         item: action.payload,
       };
     }
     case GET_FILE_SUCCESS: {
       return {
         ..state,
         file: action.payload,
       };
     }
     default:
       return state;
  }
}
// effects

export function* onGetItem(action) {
  const { payload: { fileId } } = action;
  yield put(getFile(fileId));
}

export function* itemSaga() {
  yield takeEvery(GET_ITEM_SUCCESS, onGetItem);
}

// root saga
export function* rootSaga() {
  yield spawn(itemSaga);
}

redux-saga unit tests

define('item saga', ()=>{
  const action = {
    type: GET_ITEM_SUCCESS,
    payload: { fileId: 42 },
  };

  it ('should process GET_ITEM_SUCCESS', ()=> {
    const gen = itemSaga();
    const getState = gen.next().value;
    const nextState = fork(takeEvery, GET_ITEM_SUCCESS, onGetItem);
    expect(getState).toEqual(nextState);
  });

  it ('should fire GET_FILE on GET_ITEM_SUCCESS', ()=> {
    const gen = onGetItem(action);
    const getState = gen.next().value;
    const nextState = put(getFile(fileId));
    expect(getState).toEqual(nextState);
  });
});

Cons:

  • tests are extremely verbose
  • tests are state oriented
  • tests are on the same abstract layer as the code

redux-reducer-actions

// reducer
export const reducer(state, action) {
  switch(action.type) {
     case GET_ITEM_SUCCESS: {
       const { payload: { fileId } } = action;
       const actions = [getFile(fileId)];
       return {
         ..state,
         item: action.payload,
         actions,
       };
     }
     case GET_FILE_SUCCESS: {
       return {
         ..state,
         file: action.payload,
       };
     }
     default:
       return state;
  }
}
// unit tests

define('reducer', ()=>{
  const action = {
    type: GET_ITEM_SUCCESS,
    payload: { fileId: 42 },
  }
  it ('should fire GET_FILE on GET_ITEM_SUCCESS', ()=> {
    const state = {};
    const newState = reducer(state, action);
    expect(state.actions).toBeDefined();
    expect(state.actions.length).toBe(1);
    expect(state.actions[0]).toEqual(getFile(42));
  });
});

redux-reducer-actions: attach to store

// createStore
const sagaMiddleware = createSagaMiddleware();

const middlewares = [sagaMiddleware];
// additional middlewares
// ...
const enhancer = applyMiddleware(...middlewares);

const actionEnchancer = createActionsEnhancer({});

const store = createStore(rootReducer, compose(actionEnchancer, enhancer));

sagaMiddleware.run(rootSaga);

export default function configureStore() {
  return {
    store: {
      ...store,
      runSaga: sagaMiddleware.run,
    },
  };
}

redux-reducer-actions: options

// log
// log will be used to verbose procecced actions
const isDev = process.env.NODE_ENV !== 'production';
const actionEnchancer = createActionsEnhancer({ log: isDev ? console.log.bind(console) : null });

// startActionType
// all actions which was fired before the startAction will be queried 
//     and processed AFTER the start action
const actionEnchancer = createActionsEnhancer({ startActionType: AUTH_SUCCESS });

// schedule will be used to fire actions in new event loop
// default is window.setTimeout
const actionEnchancer = createActionsEnhancer({ schedule: window.setTimeout });

Useful redux libs

  • combine-section-reducers
  • connected-react-router
  • redux-persist
  • reselect
  • redux-saga-requests

combine-section-reducers: Use root state

import combineSectionReducers from 'combine-section-reducers';

export rootReducer = 
combineSectionReducers({
  user,
  item,
  //...
});

// reducer
export const reducer(state, action, rootState) {
  switch(action.type) {
     case GET_ITEM_SUCCESS: {
       const { payload: { fileId } } = action;
       const { user: { language } } = rootState;
       const actions = [getFile(fileId, language)];
       return {
         ..state,
         item: action.payload,
         actions,
       };
     }
     case GET_FILE_SUCCESS: {
       return {
         ..state,
         file: action.payload,
       };
     }
     default:
       return state;
  }
}

Questions?

https://github.com/paullasarev/redux-reducer-actions.git

MIT License

Readme

Keywords

Package Sidebar

Install

npm i redux-reducer-actions

Weekly Downloads

1

Version

1.0.0

License

MIT

Unpacked Size

1.36 MB

Total Files

16

Last publish

Collaborators

  • paullasarev