redux-toolbelt
    TypeScript icon, indicating that this package has built-in type declarations

    3.1.1 • Public • Published

    Redux-Toolbelt

    A set of tools for quicker, easier, less verbose and safer redux development by welldone-software.
    Written in ES6.

    redux-toolbelt logo

    TOC

    Article

    Read about redux-toolbelt here

    Installation

    The tools are available in the redux-toolbelt npm package.

    npm install --save redux-toolbelt
     
    # or 
     
    yarn add redux-toolbelt

    Usage

    import the functions you like to use using one of the two methods:

    import { composeReducers, makeActionCreator } from 'redux-toolbelt';
     
    // or
     
    import composeReducers from 'redux-toolbelt/lib/composeReducers';
    import makeActionCreator from 'redux-toolbelt/lib/makeActionCreator';

    Demo

    A demo project can be found here:

    https://github.com/welldone-software/redux-toolbelt-demo

    The demo can be run in a live sandbox environment here:

    https://codesandbox.io/s/github/welldone-software/redux-toolbelt-demo

    API Reference

    makeActionCreator()

    Create an FSA complient action creator that exposes its TYPE as static member.
    This can help force type-safety without adding a lot of verbose code and constants.

    All produced actions will have a type, payload and meta properties.

    const myAction = makeActionCreator('MY_ACTION');
     
    // Usage in reducer
    const myReducer = (state, action) => {
      switch (action.type) {
        case myAction.TYPE:
          // ... responding to action
          return newState;
        default:
          return state;
      }
    };

    Adding payload and metadata to actions

    The actions creators excepts payload and meta as argument by default.

    myAction({ val: 5 }, { debug: true });
    // ==> {
    //   {
    //     type: 'MY_ACTION',
    //     payload: {val: 5},
    //     meta: {debug: true}
    //   }
    // }

    To customize the action creators the make them more clear you can use the second parameter argMapper.

    const myAction = makeActionCreator('MY_ACTION', (val, debug = false) => ({
      payload: { val },
      meta: { debug }
    }));
     
    myAction(5, true);
    // ==> {
    //   {
    //     type: 'MY_ACTION',
    //     payload: {val: 5},
    //     meta: {debug: true}
    //   }
    // }

    Actions Defaults

    There are situations where you want to creates actions that have logical relations with each other with a prefix, or a common default metadata. You can do it like so:

    const makeCounterAction = makeActionCreator.withDefaults({
      prefix: 'COUNTER/',
      defaultMeta: { log: true }
    });
     
    const increase = makeCounterAction('INCREASE');
    const decrease = makeCounterAction('DECREASE');
     
    increase();
    // ==> { type: 'COUNTER/INCREASE', meta: {log: true} }
     
    decrease();
    // ==> { type: 'COUNTER/DECREASE', meta: {log: true} }

    makeReducer()

    Creates a reducer that handles action creator[s] created with makeActionCreator.

    • The first argument is actionCreator[s] and it can be one of the following:

      • actionCreator

        const toggleAction = makeActionCreator('TOGGLE');
        const toggleReducer = makeReducer(toggleAction, state => !state, {
          defaultState: false
        });
      • An array of actionCreator's:

        const countUpdatedReducer = makeReducer(
          [increaseBy, decreaseBy],
          (state, {payload}) => (state || (payload !== 0)),
          { defaultState: false }
        })
      • An object of actionCreator's

        note: You can pass action creators as the keys of the action creator's object as well as action creator types, because they are converted to strings as part of JS specification:

        const reducer = makeReducer(
          {
            [increaseBy]: (state, { payload }) => state + payload,
            [decreaseBy.TYPE]: (state, { payload }) => state - payload
          },
          { defaultState: 100 }
        );
    • The second argument is the handler for the specified action.

      • If not specified, the reducer will update the state to the payload of the action whenever it is fired.
    • The last argument is options and it is optional. It currently receives the only parameter:

      • defaultState: Specifies the initial state. It is null by default.
    const toggle = makeActionCreator('TOGGLE');
     
    const visibilityState = makeReducer(toggleActionCreatora, visible => !visible, {
      defaultState: true
    });
     
    let state = reducer(undefined, { TYPE: '@@redux/INIT' });
    // state === true
     
    state = reducer(state, toggle());
    // state === false
     
    state = reducer(state, toggle());
    // state === true

    Passing multiple action creators as the first argument:

    const increaseBy = makeActionCreator('INCREASE_BY')
    const decreaseBy = makeActionCreator('DECREASE_BY')
     
    const countUpdatedReducer = makeReducer(
      [increaseBy, decreaseBy],
      (state, {payload}) => (state || (payload !== 0)),
      { defaultState: false }
    })
     
    let state = countUpdatedReducer(undefined, {type: '@@redux/INIT'})
    // state === false
     
    state = countUpdatedReducer(state, increaseBy(0))
    // state === false
     
    state = countUpdatedReducer(state, decreaseBy(20))
    // state === true
     
    state = countUpdatedReducer(state, increaseBy(20))
    // state === true

    Using an actions object:

    const increaseBy = makeActionCreator('INCREASE_BY');
    const decreaseBy = makeActionCreator('DECREASE_BY');
     
    // notice how passing an action creator is equivalent
    // to passing the action creator's type.
     
    const reducer = makeReducer(
      {
        [increaseBy]: (state, { payload }) => state + payload,
        [decreaseBy.TYPE]: (state, { payload }) => state - payload
      },
      { defaultState: 100 }
    );
     
    let state = reducer(undefined, { type: '@@redux/INIT' });
    // state === 100
     
    state = reducer(state, increaseBy(10));
    // state === 110
     
    state = reducer(state, decreaseBy(20));
    // state === 90

    It is very useful with composeReducers:

    const setUserName = makeActionCreator('SET_USER_NAME');
    const toggleShow = makeActionCreator('TOGGLE_SHOW');
     
    const reducer = composeReducers({
      userName: makeReducer(setUserName),
      show: makeReducer(toggleShow, state => !state, { defaultState: true })
    });
     
    const initialState = reducer(undefined, { type: '@@redux/INIT' });
    // initialState ==> {
    //   userName: null,
    //   show: true,
    // }
     
    const state1 = reducer(initialState, setUserName('test-user-name'));
    // state1 ==> {
    //   userName: 'test-user-name',
    //   show: true,
    // }
     
    const state3 = reducer(state2, toggleShow());
    // state3 ==> {
    //   userName: 'test-user-name',
    //   show: false,
    // }

    makeAsyncActionCreator()

    Wrapper around makeActionCreator(), to help create multiple actions creators for usage in async/side effects middlewares like redux-thunk, redux-saga or redux-observable.

    const fetchTodos = makeAsyncActionCreator('FETCH_TODOS');
     
    // Dispatching
    fetchTodos();
    // ==> { type: 'FETCH_TODOS@ASYNC_REQUEST' }
     
    fetchTodos.success();
    // ==> { type: 'FETCH_TODOS@ASYNC_SUCCESS' }
     
    fetchTodos.failure();
    // ==> { type: 'FETCH_TODOS@ASYNC_FAILURE' }
     
    fetchTodos.progress();
    // ==> { type: 'FETCH_TODOS@ASYNC_PROGRESS' }
     
    fetchTodos.cancel();
    // ==> { type: 'FETCH_TODOS@ASYNC_CANCEL' }
     
    // inside reducers
    // Usage in reducer
    const myReducer = (state, action) => {
      switch (action.type) {
        case fetchTodos.TYPE:
          // ... responding to request start
          return newState;
     
        case fetchTodos.success.TYPE:
          // ... responding to a successful request
          return newState;
     
        case fetchTodos.failure.TYPE:
          // ... responding to a failed request
          return newState;
     
        case fetchTodos.progress.TYPE:
          // ... responding to progress indications
          return newState;
     
        case fetchTodos.cancel.TYPE:
          // ... responding to request cancellation
          return newState;
     
        default:
          return state;
      }
    };

    makeAsyncReducer()

    Creates a reducer that handles action created with makeAsyncActionCreator(). Behavior can be defined in an options object passed as the 2nd arg:

    const asyncAction = makeAsyncActionCreator('ASYNC_ACTION');
     
    // These are the default options
    const options = {
      dataProp: 'data',
      shouldDestroyData: true,
      shouldDestroyDataOnError: true,
      shouldSetError: true,
      defaultData: undefined,
      shouldSpread: false,
      shouldSetData: true,
      dataGetter: undefined
    };
     
    const asyncReducer = makeAsyncReducer(asyncAction, options);

    Reducer Behvaiour

    Reducers created with makeAsyncReducer() respond to the request, progress, success and failure actions.

    Initialization

    On start, the reducer will return the following state by default:

    const asyncReducer = makeAsyncReducer(asyncAction);
     
    const state = undefined;
    asyncReducer(state, { type: '@@INIT' });
    // ==> {
    //   loading: false,
    //   loaded: false,
    //   data: undefined
    // }

    You can customize the data field name or default value.

    const asyncReducer = makeAsyncReducer(asyncAction, {
      dataProp: 'results',
      defaultData: []
    });
     
    const state = undefined;
    asyncReducer(state, { type: '@@INIT' });
    // ==> {
    //   loading: false,
    //   loaded: false,
    //   results: []
    // }

    You can customize the data field value that is assigned during the success reducer using the dataGetter option.

    const asyncReducer = makeAsyncReducer(asyncAction, {
      dataGetter: ({ data }, { payload }) => [...data, payload]
    });
     
    const state = { data: ['a'] };
    asyncReducer(state, asyncAction.success('b'));
    // ==> {
    //   loading: false,
    //   loaded: true,
    //   data: ['a', 'b']
    // }

    Without the dataGetter the payload replaces the old data. If you provide a dataGetter, it is called and the data it returns is the one that is used to replace the old data. This allows you to add items, remove and do what ever you need to create the new data from the current state and action.

    You can remove the use of the dataProp.

    const asyncReducer = makeAsyncReducer(asyncAction, {
      shouldSpread: true,
      defaultData: {
        counter: 0,
        status: 'offline'
      }
    });
     
    const state = undefined;
    asyncReducer(state, { type: '@@INIT' });
    // ==> {
    //   loading: false,
    //   loaded: false,
    //   counter: 0,
    //   status: 'offline'
    // }

    Please note however that we do not recommend to use shouldSpread. We have gained a lot from using the data member. The separation of the state to maintenance information and actual data, as well as adhering to such naming convention proves invaluable in terms of readability and consistency and highly increases the number of opportunities for logic reuse especially by making it clearer and easier to apply logic on state generically.

    Request

    When the reducer gets the request action it updates the loading field. The 'loaded' field keeps its value.

    const asyncReducer = makeAsyncReducer(asyncAction);
     
    const state = { loading: false, data: [1, 2, 3] };
    asyncReducer(state, asyncAction());
    // ==> {
    //   loading: true,
    //   loaded: state.loaded,
    //   data: [1, 2, 3]
    // }

    You can also configure the reducer to destroy the current results.

    const asyncReducer = makeAsyncReducer(asyncAction, {
      shouldDestroyData: true,
      defaultData: []
    });
     
    const state = { loading: false, data: [1, 2, 3] };
    asyncReducer(state, asyncAction());
    // ==> {
    //   loading: true,
    //   loaded: false,
    //   data: []
    // }
    Progress

    When the reducer gets the progress action it's updating the progress field with the action's payload. The 'loaded' field depends on the previous value - indicating if the data is already loaded

    const asyncReducer = makeAsyncReducer(asyncAction);
     
    const state = { loading: true };
    asyncReducer(state, asyncAction.progress(5));
    // ==> {
    //   loading: true,
    //   loaded: state.loaded,
    //   progress: 5
    // }
    Success

    When the reducer gets the success action is updates the loading to false, the 'loaded' to 'true' and sets the dataProp field with the action's payload.

    const asyncReducer = makeAsyncReducer(asyncAction);
    const state = { loading: true };
    asyncReducer(state, asyncAction.success([1, 2, 3]));
    // ==> {
    //   loading: false,
    //   loaded: true,
    //   'data': [1, 2, 3]
    // }

    If the data isn't needed you can remove it from the state completely.
    In this way you only detect requests success and failure.

    const asyncReducer = makeAsyncReducer(asyncAction, {
      shouldSetData: false
    });
     
    const state = { loading: true };
    asyncReducer(state, asyncAction.success([1, 2, 3]));
    // ==> {
    //   loading: false,
    //   loaded: true,
    // }
    Failure

    When the reducer gets the failure action is updates the loading to false and the error field with the action's payload.

    const asyncReducer = makeAsyncReducer(asyncAction);
     
    const state = { loading: true };
    asyncReducer(state, asyncAction.failure(`Server unreachable`));
    // ==> {
    //   loading: false,
    //   error: 'Server unreachable',
    //   loaded: state.loaded,
    // }

    composeReducers()

    This function in a replacement for redux's combineReducers.
    The function excepts multiple reducers as arguments and executes them one after the other.
    If in argument is a reducers map instead of a reducer (like in composeReducers) we create a reducer from that map in the same fashion.

    const mainReducer = (state, action) => {...} // returns { todos: [...] }
    const sideEffectReducer = (state, action) => {...} // return { count: 0 }
    const currentActionReducer = (state, action) => {...} // return 'dragging'
    const userNameReducer = (state, action) => {...} // return 'welldone'
     
    // reducer will return
    // {
    //   todos: [...],
    //   count: 3,
    //   currentAction: 'dragging',
    //   userName: 'welldone'
    // }
    const reducer = composeReducers(
      mainReducer,
      sideEffectReducer,
      {
        currentAction: currentActionReducer,
        userName: userNameReducer
      }
    )

    Default State

    As a result of the reducers run one after the other, only the first one will get an undefined state on the first run.
    Nested reducers will get undefined if none was supplied in the root state:

    const DEFAULT_STATE = {val: 1}
    const IGNORED = {anotherVal: 2}
    const NESTED_DEFAULT_STATE = 5
     
    const mainReducer = (state = DEFAULT_STATE, action) => {...}
    const anotherReducer = (state = IGNORED_STATE, action) => {...}
    const nestedReducer = (state = NESTED_DEFAULT_STATE, action) => {...} // will get NESTED_DEFAULT_STATE on first run (or what mainReducer returns)
     
    const reducer = composeReducers(
      mainReducer, // will get DEFAULT_STATE on first run
      anotherReducer, // will get DEFAULT_STATE on first run (or what mainReducer returns)
      {
        nestedVal: nestedReducer // will get NESTED_DEFAULT_STATE on first run
      }
    )

    isActionCreator()

    A utility to determine if an object is an action creator:

    const a = makeActionCreator('A');
    //isActionCreator(a) === true
     
    const b = makeAsyncActionCreator('B');
    // isActionCreator(b) === true
    // isActionCreator(b.success) === true
    // isActionCreator(b.failure) === true
    // isActionCreator(b.progress) === true
    // isActionCreator(b.cancel) === true
     
    // isActionCreator({}) === false
    // isActionCreator(true) === false
    // isActionCreator(() => {}) === false
    // isActionCreator([]) === false

    Typing

    Package now supports typescript.

    Keywords

    none

    Install

    npm i redux-toolbelt

    DownloadsWeekly Downloads

    2,160

    Version

    3.1.1

    License

    MIT

    Unpacked Size

    122 kB

    Total Files

    57

    Last publish

    Collaborators

    • vzaidman
    • danikenan