redux-heretic

2.1.1 • Public • Published

Redux Heretic

Reduce the amount of boilerplate code needed to create a Redux app, by merging actions and reducers into the same definition 🙀.

Examples

Counter

Plain redux counter example:

// Reducer
function counter(state, action) {
  if (typeof state === 'undefined') {
    return 0;
  }
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}
 
// Action factory
function increment() {
  return {type: 'INCREMENT'};
}
 
function decrement() {
  return {type: 'DECREMENT'};
}
 
const store = Redux.createStore(counter);
store.dispatch(increment());

Using redux-heretic:

Each key of the object passed to redux-heretic contains a reducer function. The returned object has action factories and a reducer that will apply the state change based on the action type.

import h from 'redux-heretic';
 
const {actions, reducer} = h(
  {
    increment: state => state + 1,
    decrement: state => state - 1
  },
  {initialState: 0}
);
 
const store = Redux.createStore(reducer);
store.dispatch(actions.increment());

Custom action factory

Each value in the object passed to redux-heretic accepts a function or an object.

The function variant specifies a reducer. The object variant allows to pass an action factory and/or a reducer.

const {actions, reducer} = h(
  {
    add: {
      // This will be used to create the action
      // type & actions are values passed by the library:
      //
      // type = the action type, by default the library converts
      // 'add' to UPPER_SNAKE_CASE, e.g ADD
      //
      // actions = the same actions object returned by
      // the h function, so you can access other actions.
      // See the async example bellow.
      create: (type, actions, value) => ({type, amount: value}),
 
      // This will be used to return the new state when
      // the action is received
      reduce: (state, action) => state + action.amount
    }
  },
  {initialState: 0}
);
 
const store = Redux.createStore(reducer);
 
// add(10) calls the create function with: create('ADD', 10) to
// get value of the action
store.dispatch(actions.add(10));
 
// you can also access to the action type
console.log(action.add.type); // --> ADD

Prefix action types

When combining reducers, you need to make sure that action types are unique. You can pass a prefix, and all the action types will use it.

const {actions, reducer} = h(
  {
    increment: state => state + 1,
    decrement: state => state - 1
  },
  {
    initialState: 0,
 
    // every action type is prefixed with COUNTER_
    prefix: 'counter'
  }
);
 
console.log(actions.increment.type); // --> COUNTER_INCREMENT

Async action with redux-thunk

You can define an action factory without a reducer. Also, each action factory receives an actions instance, so you can create other actions to dispatch.

This is helpful when using libraries like redux-thunk.

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import h from 'redux-heretic';
 
const {actions, reducer} = h({
  increment: {
    create(type, actions, payload) {
      return dispatch => {
        // use the actions objects to easily create other actions,
        // note that you can also hardcode {type: 'START_INCREMENT'}
        dispatch(actions.startIncrement());
        setTimeout(() => dispatch(actions.finishIncrement()), 1000);
      }
    }
    // You can skip the reducer function if you don't want it
  },
 
  startIncrement(state) {
    return Object.assign({}, state, {
      requestInProgress: true
    });
  }
 
  finishIncrement(state) {
    return Object.assign({}, state, {
      requestInProgress: false,
      value: state.value + 1
    });
  }
},
{  
  initialState: {
    requestInProgress: false,
    value: 0
  }
});
 
const store = Redux.createStore(reducer, applyMiddleware(thunk));
store.dispatch(actions.increment());

Custom action type names

By default, action names have the format: PREFIX_ACTION_NAME.

For example startIncrement with the prefix counter, becomes COUNTER_START_INCREMENT.

To change the format pass a typeFormat function to the options:

import h from 'redux-heretic';
 
const {actions, reducer} = h(
  {
    startIncrement(state) {
      return Object.assign({}, state, {
        requestInProgress: true
      });
    }
  },
  {
    initialState: {
      requestInProgress: false
    },
    prefix: 'a',
    typeFormat: (name, prefix) => `${name}/${prefix}`
  }
);
 
console.log(actions.startIncrement.type);
// shows: startIncrement/a

Non 1-1 relationship between action-reducer

Redux Heretic makes easy to create a 1-1 relationship between action and reducer. But, actions are not necessarily related to one reducer. Is possible to define only action factories using create:

const {actions} = h(
  {
    increment: {
      create(type, actions, payload) {
        return dispatch => {
          dispatch(startIncrement());
          setTimeout(() => dispatch(finishIncrement()), 1000);
        };
      }
    }
  },
  {
    prefix: 'counter'
  }
);
 
// we only care of action factories
export default actions;

In that case, the library also helps to reduce the amount of boilerplate code using the actionFactories helper:

import {actionFactories} from 'redux-heretic/helpers';
 
export default actionFactories(
  {
    increment(type, actions, payload) {
      return dispatch => {
        dispatch(startIncrement());
        setTimeout(() => dispatch(finishIncrement()), 1000);
      };
    }
  },
  'counter'
);

Heads up! Note the /helpers module path.

But what about reducers? Do I need to code the usual switch statement to handle an action defined elsewhere?

You can use Redux Heretic for that too, but it's a little bit verbose:

const {reducer} = h(
  {
    [myActionFactory.type]: {
      reduce(state, action) {
        // ...
        return {...state /* ... */};
      }
    }
  },
  {
    typeFormat: name => name
  }
);
export default reducer;

For those cases the library provides the handy reducer function:

import {reducer} from 'redux-heretic/helpers';
export default reducer({
  [myActionFactory.type]: (state, action) => {
    // ...
    return {...state /* ... */};
  }
});

Reducing boilerplate code of action factories

The previous section describes how to write less code when creating action factories without a reducer. If your action factories are dumb, you can reduce the amount of code even more with the helpers defaultActionFactory and defaultActionFactories.

Before:

import {actionFactories} from 'redux-heretic/helpers';
export default actionFactories({
  dumbAction: (type, actions, payload) => ({type, ...payload})
});

With defaultActionFactory:

import {actionFactories, defaultActionFactory} from 'redux-heretic/helpers';
export default actionFactories({
  dumbAction: defaultActionFactory
});

Using defaultActionFactories:

import {actionFactories, defaultActionFactories} from 'redux-heretic/helpers';
export default actionFactories(
  defaultActionFactories('dumbAction1', 'dumbAction2', 'dumbAction3')
);

License

MIT

Readme

Keywords

none

Package Sidebar

Install

npm i redux-heretic

Weekly Downloads

0

Version

2.1.1

License

MIT

Unpacked Size

12.4 kB

Total Files

8

Last publish

Collaborators

  • dfernandez79