1.3.0 • Published 3 months ago

redux-mill v1.3.0

Weekly downloads
3
License
MIT
Repository
github
Last release
3 months ago

redux-mill

  1. What is it?
  2. Reasons to use it
  3. Installation
  4. Using
  5. Without and with comparison
  6. Tests

What is it?

redux-mill is the easy way to use redux by one object. Main idea is possibility to use only one object to use actions, action creators and store.

You should try it if

  • You hate a lot of actions constants and very long imports of these constants
  • You hate a lot of equal action creators and very long imports
  • You want to use reducer path once in one place

Installation

npm install --save redux-mill

Using

It is easy to understand but there are some details.

Create your reducer

  // myReducer.js
  import reduxMill from 'redux-mill';

  const initialState = { title: 'Default title' };

  const reducer = {
    EDIT_TITLE: (state, title) => ({ ...state, title }),
    // ACTION_TYPE: (state, payload, action) => ({ ...state, ...}),
    SAVE: {
      // it is for SAVE action
      0: state => ({ state, saving: true, error: "" }),
      // it is for SAVE__END action
      END: (state, payload, action) => ({ ...state, saving: false }),
       // it is for SAVE__FAIL action
      FAIL: (state, payload, action) => ({ ...state, saving: false, error: payload.error })
    }
    // ...
  };

  export const store = reduxMill(
    initialState,
    reducer,
    'myStoreName',
    {
      // debug: true,
      // stateDebug: true,
      // nameAsPrefix: false,
      // if nameAsPrefix is true, then your action types will looks like -
      // `{storeName}{divider}{actionType}` => `myStoreName__EDIT_TITLE`
      // or `myStoreName__SAVE__FAIL` for children actions
      divider: "__",
      // it is divider between children and parent parts of key - SAVE__FAIL
      mainKey: 0
      // it is children key that means reaction for parent action
      // LOAD: { 0: state => ({ ...state }) }
      // is the same that
      // LOAD: state => ({ ...state })
    }
  );

  // selectors
  export const selectTitle = store(state => state.title);
  // ...

Connecting to your store

  // store.js
  import { createStore, applyMiddleware } from "redux";
  import { store: myReducer } from './myReducer';
  import { store: mySecondReducer } from './mySecondReducer';

  const reducers = {
    ...myReducer,
    // it will be 'myStoreName' key
    ...mySecondReducer
  };

  export default createStore(
    combineReducers(reducers),
    applyMiddleware(...)
  );

Using action creator in component

  import React, { PureComponent } from 'react';
  import { connect } from 'react-redux';
  import myReducer, { selectTitle } from './myReducer';

  class MyComp extends PureComponent {
    onClick = () => {
      const { changeTitle } = this.props;
      changeTitle('Title ' + Math.random());
    }

    render() {
      return (
        <div>
          <h3>{title}</h3>
        <button onClick={this.onClick}>
        </button>
        </div>
      )
    }
  }

  export default connect(
    (state) => ({
      title: selectTitle(state),
    }),
    {
      changeTitle: myReducer.EDIT_TITLE
      // myReducer.EDIT_TITLE is action creator function(payload)
    }
  )(MyComp);

(!) reducer object will be changed after call of reduxMill

It will be object with the same structure, but each value will be action creator function(payload).

  import reduxMill from 'redux-mill';

  const reducer = {
    EDIT_TITLE: (state, title) => ({ ...state, title }),
    SAVE: {
      0: state => state,
      END: state => state,
      FAIL: (state, payload) => ({ ...state, ...payload })
    }
  };
  export const store = reduxMill(
    initialState,
    reducer,
    'myStoreName',
    { divider: "__" },
  );

  // what will be in `reducer` object after reduxMill calling
  {
    EDIT_TITLE: function(payload) {
      this._ = 'EDIT_TITLE';
      this.type = 'EDIT_TITLE';
      this.toString = function() { return 'EDIT_TITLE' };
      this.valueOf = function() { return 'EDIT_TITLE' };
      return { type: 'EDIT_TITLE': payload };
    },
    SAVE: function(payload) {
      this.END = function(payload) {
        this._ = 'SAVE__END';
        this.type = 'SAVE__END';
        this.toString = function() { return 'SAVE__END' };
        this.valueOf = function() { return 'SAVE__END' };
        return { type: 'SAVE__END': payload };
      },
      this.FAIL = function(payload) {
        this._ = 'SAVE__FAIL';
        this.type = 'SAVE__FAIL';
        this.toString = function() { return 'SAVE__FAIL' };
        this.valueOf = function() { return 'SAVE__FAIL' };
        return { type: 'SAVE__FAIL': payload };
      },
      this._ = 'SAVE';
      this.type = 'SAVE';
      this.toString = function() { return 'SAVE' };
      this.valueOf = function() { return 'SAVE' };
      return { type: 'SAVE': payload };
    }
  };

  // hot it can be used
  console.log(reducer.EDIT_TITLE == 'EDIT_TITLE')
  // console:> true
  // (!!!) but
  console.log(reducer.EDIT_TITLE === 'EDIT_TITLE')
  // console:> false

  console.log(reducer.EDIT_TITLE._)
  // console:> EDIT_TITLE
  console.log(reducer.EDIT_TITLE.type)
  // console:> EDIT_TITLE
  console.log(String(reducer.SAVE.FAIL))
  // console:> SAVE__FAIL
  console.log(reducer.EDIT_TITLE({ a: 10 }))
  // console:> { type: 'EDIT_TITLE', payload: { a: 10 } }

  console.log(reducer.SAVE.END({ a: 10 }))
  // console:> { type: 'SAVE__END', payload: { a: 10 } }

So reducer can be used as action type for redux-saga

  takeEvery(reducer.EDIT_TITLE, ...);

or as action creator

  reducer.EDIT_TITLE(newTitle);

Description

reduxMill(initialState, reducer, storeName, options): function(selector: function(store, ...args));

argstypedefaultdescription
initialStateObject-default state
reducerObject-Object or instance of your class
storeNameString-it is name for redux store
optionsObject{...}additional options

Options includes next props

argstypedefaultdescription
debugBooleanfalseenable debug console logging
stateDebugBooleanfalseenable debug state changes console logging
dividerString'_'it is divider between parent key and children sub-key
nameAsPrefixBooleanfalseenable adding storeName as prefix for action types - {storeName}{divider}{actionType}
mainKeyString0it is key for main handler for parent action name
reducerWrapperFunction(initState, Function)It is function wrapper for your reducer (e.g. immer)
actionWrapperFunction(Function)It is function wrapper for each action
actionCreatorWrapperFunction(Function, actionType)It is function wrapper for each action creator

It returns function that is factory of selectors for REDUXstoreName

  const store = reduxMill(initialState, reducer, 'storeName');
  const selectItem = store((state, id) => state.items[id]);
  // it returns function(state, id)
  const selectItem2 = (state, id) => state['storeName'].items[id];
  // selectItem the same that selectItem2
  // state['storeName'] - is our current store
  // ...

Use with reducerWrapper

If you want to wrap your reducer, then you should use reducerWrapper. When we make reducer function we will wrap it into reducerWrapper(initialState, reducerFunction). reducerWrapper should return Function(state, action).

  import reduxMill from 'redux-mill';

  function wrapReducer(baseState, callback) {
    // callback - it is reducer function(state, action), returns state
    console.log('your reducer was wrapped', baseState, callback);
    return (state = baseState, action) => {
      console.log('reducer called with action: ', action);
      console.log('we will generate new state each time!');
      // don't repeat this trick with JSON.parse(JSON.stringify(...))
      // it will be applied for any result of reducer
      // and for current state if this reducer doesn't contain required action
      return JSON.parse(JSON.stringify(callback(state, action)));
    }
  }

  const initialState = {
    title: '',
    items: [
      { note: '11111' },
    ]
  }

  const reducer = {
    setTitle: (draft, payload) => { draft.title = payload }
    setNote: (draft, payload) => { draft.items[0].note = payload }
  }

  const store = reduxMill(initialState, reducer, 'storeName', { reducerWrapper: wrapReducer });

Test cases for reducerWrapper

Use with actionWrapper

If you want to wrap each action handler separately then you should use actionWrapper. Each action handler will be wrapped into actionWrapper(actionHandler). We expect that actionHandler is Function(state, action). actionWrapper should return Function(state, [payload, action]).

  import reduxMill from 'redux-mill';

  function wrapAction(callback) {
    // callback - it is action function(state, payload, action), returns state
    console.log('your action handler was wrapped', callback);
    return (state, payload, action) => {
      console.log('Action was called: ', action.type);
      console.log('we will generate new state');
      // I don't recommend to use this trick with JSON.parse(JSON.stringify(...))
      // But you can, because it will be called only for actions, not for default
      return JSON.parse(JSON.stringify(callback(state, payload, action)));
    }
  }

  const initialState = {
    title: '',
    items: [
      { note: '11111' },
    ]
  }

  const reducer = {
    setTitle: (draft, payload) => { draft.title = payload },
    setNote: (draft, payload) => { draft.items[0].note = payload }
  }

  const store = reduxMill(initialState, reducer, 'storeName', { actionWrapper: wrapAction });

  // if you wont to use actionWrapper, then you should wrap each handler by your self, it is the same
  // const reducer = {
  //   setTitle: produceAction((draft, { payload }) => { draft.title = payload }),
  //   setNote: produceAction((draft, { payload }) => { draft.items[0].note = payload })
  // }

Test cases for actionWrapper

Use with actionCreatorWrapper

If you want to wrap each action creator then you should use actionCreatorWrapper. Each action creator will be wrapped into actionCreatorWrapper(actionCreator, actionType). We expect that actionCreator is Function(payload). actionCreatorWrapper should return Function(...anyArgs).

  import reduxMill from 'redux-mill';

  function wrapActionCreator(baseActionCreator, actionType) {
    // we may return `baseActionCreator` based on any condition, e.g. `actionType` comparison
    if (actionType === 'setColor') return baseActionCreator;
    // baseActionCreator - it is action creator function(payload), returns { type: String, payload: Any }
    console.log('your action creator was wrapped', baseActionCreator);
    return (payload, addTime = false) => {
      const action = baseActionCreator(payload);
      if (addTime) {
        action.meta = { ...action.meta, currentTime: Date.now() };
      }
      console.log('Action creator was wrapped for: ', action.type);
      return action;
    }
  }

  const initialState = {
    title: '',
  }

  const reducer = {
    setTitle: (state, payload, action) => ({
      ...state,
      title: payload + ' ' + action.meta.currentTime
    }),
    setColor: (state, payload) => ({ ...state, color: payload })
  }

  const store = reduxMill(
    initialState,
    reducer,
    'storeName',
    { actionCreatorWrapper: wrapActionCreator }
  );

  // In this example `reducer` will look like:
  // {
  //   setTitle: wrapActionCreator((payload) => ({ type: 'setTitle', payload })),
  //   setColor: (payload) => ({ type: 'setColor', payload })
  // }

Test cases for actionCreatorWrapper

reducer is simple object

  import { select } from "js-split";
  import mill from "./mill";

  const storeName = "myItems";
  const initialState = {
    items: [],
    title: "",
    loading: false,
    adding: false,
    removing: false,
    error: false
  };

  const END = "END";
  const FAIL = "FAIL";

  const reducer = {
    EDIT_TITLE: (state, title) => ({ ...state, title }),
    GET_ITEMS: {
      0: state => ({ ...state, loading: true, error: "" }),
      [END]: (state, payload) =>
        ({ ...state, loading: false, items: payload }),
      [FAIL]: state =>
        ({
          ...state,
          loading: false,
          error: "Something is wrong with items =("
        })
    },
    ADD_ITEM: {
      0: state => ({ ...state, adding: true, error: "" }),
      [END]: (state, payload) => ({ ...state, adding: false, items: payload }),
      [FAIL]: state =>
        ({ ...state, adding: false, error: "The same item there is in list" })
    },
    REMOVE_ITEM: {
      0: state => ({ ...state, removing: true, error: "" }),
      [END]: (state, payload) =>
        ({ ...state, removing: false, items: payload }),
      [FAIL]: state =>
        ({ ...state, removing: false, error: "It can't be removed" })
    }
  };
  // Use the same handler for another action
  reducer.DELETE_ITEM = reducer.REMOVE_ITEM;

  export const store = mill(initialState, reducer, storeName, {
    divider: "__",
    mainKey: 0
  });

  export const selectItems = store(state => state.items);
  export const selectTitle = store(state => state.title);
  export const selectError = store(state => state.error);

  export default reducer;

reducer is instance of function

  import { select } from "js-split";
  import mill from "./mill";

  const storeName = "myItemsM";
  const initialState = {
    items: [],
    title: "",
    loading: false,
    adding: false,
    removing: false,
    error: false
  };

  const END = "END";
  const FAIL = "FAIL";

  const reducer = new function() {
    this.EDIT_TITLE = (state, title) => ({ ...state, title });
    this.GET_ITEMS = {
      0: state => ({ ...state, loading: true, error: "" }),
      [END]: (state, payload) =>
        ({ ...state, loading: false, items: payload }),
      [FAIL]: state =>
        ({
          ...state,
          loading: false,
          error: "Something is wrong with items =("
        })
    };

    this.ADD_ITEM_BASE = {
      0: state => ({ ...state, adding: true, error: "" }),
      [END]: (state, payload) => ({ ...state, adding: false, items: payload }),
      [FAIL]: state =>
        ({ ...state, adding: false, error: "The same item there is in list" })
    };
    this.ADD_ITEM = {
      0: state => this.ADD_ITEM_BASE[0],
      [END]: (state, payload) => ({ ...state, adding: false, items: payload }),
      [FAIL]: state =>
        ({
          ...state,
          adding: false,
          error: "The same item  is in the list"
        })
    };

    this.REMOVE_ITEM = {
      0: state => ({ ...state, removing: true, error: "" }),
      [END]: (state, payload) =>
        ({ ...state, removing: false, items: payload }),
      [FAIL]: state =>
        ({ ...state, removing: false, error: "It can't be removed" })
    };

    // Use the same handler for another action
    this.DELETE_ITEM = this.REMOVE_ITEM;
  }();

  export const store = mill(initialState, reducer, storeName, {
    divider: "__",
    mainKey: 0
  });
  export default reducer;

  export const selectItems = store(state => state.items);
  export const selectTitle = store(state => state.title);
  export const selectError = store(state => state.error);

reducer is instance of class

  import { select } from "js-split";
  import mill from "./mill";

  const storeName = "myItemsM";
  const initialState = {
    items: [],
    title: "",
    loading: false,
    adding: false,
    removing: false,
    error: false
  };

  const END = "END";
  const FAIL = "FAIL";

  const reducer = new class {
    EDIT_TITLE = (state, title) => ({ ...state, title });
    GET_ITEMS = {
      0: state => ({ ...state, loading: true, error: "" }),
      [END]: (state, payload) =>
        ({ ...state, loading: false, items: payload }),
      [FAIL]: state =>
        ({ ...state, {
          loading: false,
          error: "Something is wrong with items =("
        })
    };

    ADD_ITEM = {
      0: state => ({ ...state, adding: true, error: "" }),
      [END]: (state, payload) => ({ ...state, adding: false, items: payload }),
      [FAIL]: state =>
        ({ ...state, adding: false, error: "The same item there is in list" })
    };

    REMOVE_ITEM = {
      0: state => ({ ...state, removing: true, error: "" }),
      [END]: (state, payload) =>
        ({ ...state, removing: false, items: payload }),
      [FAIL]: state =>
        ({ ...state, removing: false, error: "It can't be removed" })
    };

    // Use the same handler for another action
    DELETE_ITEM = this.REMOVE_ITEM;
  }();

  export const store = mill(initialState, reducer, storeName, {
    divider: "__",
    mainKey: 0
  });
  export default reducer;

  export const selectItems = store(state => state.items);
  export const selectTitle = store(state => state.title);
  export const selectError = store(state => state.error);

Without and with comparison

Before => 64 lines / 4 actions constants / 7 action creators

Alt Text

After => 47 lines / 1 action creators export

Alt Text

External lib deep-mutation was used in examples, but it IS NOT required dependency.