Skip to content

LFDM/redux-state-keys

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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<Id, User>, 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

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published