No description or website provided.
JavaScript
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
dist
src
.eslintrc
.gitignore
LICENSE
README.md
package.json

README.md

Redux State Keys

This library tries to deal with the problem of writing a lot of repetitive boilerplate code, when you are trying to deal with several instances of a same type.

This is a common pattern which typically arises in a lot of places in your application: You want to save whether a modal is open or not, you want to modify a User which is stored in a Map, save several error states etc.

For a high level overview on this topic check out Robin Wieruch's blog post about state keys.

Example

Let's look at a simple module to handle loading states:

  // the loading module

  const SLICE_NAME = 'loadingStates';

  const INITIAL_STATE = { isLoading: false };

  const SET_LOADING = 'SET_LOADING';

  const reducer = (state = INITIAL_STATE, action) => {
    if (action.type === SET_LOADING) {
      const isLoading = action.payload;
      return { ...state, isLoading };
    }
    return state;
  };

  const doSetIsLoading(isLoading) {
    return {
      type: SET_LOADING,
      payload: isLoading,
    };
  }

  const getLoadingState(state) {
    return state[SLICE_NAME].isLoading;
  }

  const reducers = {
    [SLICE_NAME]: reducer
  };

  const selectors = {
    getLoadingState
  };

  const actionCreators = {
    doSetIsLoading
  };

  export default {
    reducers,
    selectors,
    actionCreators,
  };


  // in your store file

  import { createStore, combineReducers } from 'redux';
  import loadingModule from './loadingModule';

  const combinedReducer = combineReducers({
    ...loadingModule.reducers
  });

  export default createStore(combinedReducer);

This is not terribly useful, because we can handle only one global loading state across the whole application. We therefore might need to introduce some kind of name property - a state key.

  // the loading module

  const SLICE_NAME = 'loadingStates';

  const INITIAL_STATE = {};
  const INITIAL_SUBSTATE = { isLoading: false };

  const SET_LOADING = 'SET_LOADING';

  const reducer = (state = INITIAL_STATE, action) => {
    if (action.type === SET_LOADING) {
      const { name, isLoading } = action.payload;
      const namedState = state[name] || INITIAL_SUBSTATE;
      return { ...state, [name]: { ...namedState, isLoading };
    }
    return state;
  };

  const doSetIsLoading(name, isLoading) {
    return {
      type: SET_LOADING,
      payload: { name, isLoading },
    };
  }

  const getLoadingState(state, name) {
    const namedState = state[SLICE_NAME][name] || INITIAL_SUBSTATE;
    return namedState.isLoading;
  }

  const reducers = {
    [SLICE_NAME]: reducer
  };

  const selectors = {
    getLoadingState
  };

  const actionCreators = {
    doSetIsLoading
  };

  export default {
    reducers,
    selectors,
    actionCreators,
  };

This makes our code immediately more complicated as every function now needs to deal with another level of indirection.

The helper functions in redux-state-keys allow to hide this complexity - our module can stay as simple as if we only had to deal with ONE loading state across the whole application.

  import {
    createReducerWithStateKeyHandling,
    createSelectorsWithStateKeyHandling
  } from 'redux-state-keys';

  const SLICE_NAME = 'loadingStates';

  const INITIAL_SUBSTATE = { isLoading: false };

  const SET_LOADING = 'SET_LOADING';

  const reducer = (state, action) => {
    if (action.type === SET_LOADING) {
      const isLoading = action.payload;
      return { ...state, isLoading };
    }
    return state;
  };

  const doSetIsLoading(isLoading) {
    return {
      type: SET_LOADING,
      payload: isLoading,
    };
  }

  const getLoadingState(state) {
    return state.isLoading;
  }

  const reducers = {
    [SLICE_NAME]: createReducerWithStateKeyHandling(reducer, INITIAL_SUBSTATE),
  };

  const selectors = createSelectorsWithStateKeyHandling({
    getLoadingState
  }, INITIAL_SUBSTATE, SLICE_NAME);

  const actionCreators = {
    doSetIsLoading
  };

  export default {
    reducers,
    selectors,
    actionCreators,
  };

Consuming containers can use further helper methods to shield you away from dealing with state keys manually.

// a simple presenter in a presenter.js file

export default ({ isLoading, setLoading }) => {
  return (
    <div>
      <button type="button" onClick={ () => setLoading(!isLoading) } />
      <div>
        { isLoading ? 'We are loading!' : '' }
      </div>
    </div>
  );
};


// container without redux-state-keys helper

import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { selectors, actionCreators } from '../loadingModule';
import presenter from './presenter';

function mapStateToProps(state, props) {
  const { name } = props;
  const isLoading = selectors.isLoading(state, name);
  return { isLoading };
}

function mapDispatchToProps(dispatch, props) {
  const { name } = props;
  const setLoading = (isLoading) => actionCreators.doSetIsLoading(name, isLoading);
  return bindActionCreators({ setLoading }, dispatch);
}

export default connect(mapStateToProps, mapDispatchToProps)(presenter);


// container with redux-state-keys helper

import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { bindStateKeyToActionCreators, bindStateKeyToSelectors } from 'redux-state-keys';
import { selectors, actionCreators } from '../loadingModule';
import presenter from './presenter';

function mapStateToProps(state, props) {
  const { name } = props;
  const boundSelectors = bindStateKeyToSelectors(name, selectors);
  const isLoading = boundSelectors.isLoading(state);
  return { isLoading };
}

function mapDispatchToProps(dispatch, props) {
  const { name } = props;
  const boundActionCreators = bindStateKeyToActionCreators(name, {
    setLoading: actionCreators.doSetIsLoading
  });
  return bindActionCreators(boundActionCreators, dispatch);
}

export default connect(mapStateToProps, mapDispatchToProps)(presenter);


// render our container component like this

<LoadingDemo name="someKindOfIdentifier" />

API Documentation

tbd