1
\$\begingroup\$

Lately I am answering more and more questions regarding ReactJS & Redux on SO and I created a basic playground that should help me answer questions faster.

The goal of the playground is to have some of the basic boilerplate code prepared so that I might have an easier time creating a stack snippet and that I have some of the default necessities prepared.

So, the goal of the script is:

  • To have the default scripts available for adding them to a snippet
  • Have some basic higher order functions and higher order components available
  • Have a quick way to set up a snippet to answer questions

I mostly spend quite a lot of time on:

  • getting the cdns required for running a react/redux enabled app
  • settings up some reducers & actions

To that end I created following snippet and I am asking myself the following questions:

middleware

I haven't used redux-thunk all that often in the past, and in this snippet, I just implemented a very basic middleware that is kind of promise-aware. Is this enough for basic functionality?

// a middleware that is aware of promises, but doesn't enforce them
const promiseAware = store => next => action => {
  (action.then && action.then( next )) || next( action );
};

It is used in my store, where I create something like:

// create a store, apply the promiseAware middleware
const store = createStore( combineReducers( reducers ), applyMiddleware( promiseAware ) );

CDNs

As I use CDNs, I cannot really import or require the different classes and functions offered through the frameworks. Is the following setup an easy enough replacement, and is it for an outsider understandable that these would need to be imported in a real code scenario?

First the CDNs

<script id="react" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.2/react.js"></script>
<script id="react-dom" src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/15.6.2/react-dom.js"></script>
<script id="redux" src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.7.2/redux.js"></script>
<script id="react-redux" src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.6/react-redux.js"></script>
<script id="prop-types" src="https://cdnjs.cloudflare.com/ajax/libs/prop-types/15.6.0/prop-types.js"></script>
<script id="classnames" src="https://cdnjs.cloudflare.com/ajax/libs/classnames/2.2.5/index.js"></script>

Dummy import / require like

const { createStore, combineReducers, applyMiddleware } = Redux;
const { Provider, connect } = ReactRedux;

higher order component

I created a higher order component to auto dispatch an action on the props. From my rather limited sample app where I am using it, it is working perfectly fine, I am just wondering if I need to redirect the action that gets dispatched automatically and if this component makes any sense or if I should just take the boilerplate for preloading data through componentWillMount()

  // higher order component
  // calls a certain action with default arguments
  // and maps the action so that the wrapped component handles it's call to the action
  // through the wrapped component
  // the wrapped component should still be able to have own props
  const withAutoload = (Component, action, ...initialActionArgs) => {
    return class extends React.Component {
      componentWillMount() {
        this.onLoad( ...initialActionArgs );
      }
      onLoad( ...functionArgs ) {
        return this.props[action]( ...functionArgs );
      }
      render() {
        // transform the props and make sure that the autoload action gets mapped through the wrapped component
        // this should ensure that their is no props discrepancy
        // and it allows the hoc to be "invisble" from the child components view
        const transformed = { ...this.props, [action]: (...childArgs) => this.onLoad(...childArgs)};
        return <Component {...transformed} />
      }
    }
  };

The full code of the current snippet, is available on github but for people who don't like navigating around, I also have the stacksnippet version available here (to not overload the question, it's is hidden by default).

Although I don't have real major questions about the full code, if somebody feels like reviewing it, I would be happy for some feedback.

  /*-------------------*/
  /* #region DRY setup */
  /*********************/
  const { createStore, combineReducers, applyMiddleware } = Redux;
  const { Provider, connect } = ReactRedux;

  const withDefaultState = ( reducer, defaultState = { defaultState: true } ) => {
    // ensures that the default state is returned
    // in case the reducer doesn't return anything
    // but the advantage is that I can worry about
    // code inside the reducer at a later time
    return (state, action) => {
      return reducer(state, action) || defaultState;
    }
  };

  // creates actions based on a name and some other arguments
  // eg: actions('person')('get', 'set')
  // should return an object like:
  // { get: 'person/get', set: 'person/set' }
  const actions = name => (...args) => {
    return args.reduce( (current, prop) => {
      current[prop] = `${name}/${prop}`;
      return current;
    }, {} );
  }

  // combines the state based on the arguments given
  // arguments should be string or
  // object in the form of { target: 'property', select: (object, string)(), source: 'optional state property, if not available takes target' }
  // eg: stateMapper('people')
  // creates an object with
  // { people: state.people }
  // eg: stateMapper({ target: 'malepeople', source: 'people', select: (ds, prop) => ds.filter(i => i.sex === 'm') })
  // will return
  // { malepeople: select(state[people], 'malepeople') }
  const stateMapper = (...args) => state => args.reduce( (current, prop) => {
    if (typeof prop === 'object') {
      current[prop.target] = prop.select( (prop.source && state[prop.source]) || state[prop.target], prop.target );
      return current;
    }
    current[prop] = state[prop];
    return current;
  }, {} );

  // creates a dispatcher for all actions defined that calls dispatch with the action type and sends the args as a payload
  const actionDispatcher = ( actions ) => dispatch => Object.keys(actions).reduce( (current, prop) => {
    current[prop] = (...args) => dispatch( { type: actions[prop], payload: args } );
    return current;
  }, {} );

  // a middleware that is aware of promises, but doesn't enforce them
  const promiseAware = store => next => action => {
    (action.then && action.then( next )) || next( action );
  };

  const serviceDispatcher = ( actions ) => ( service ) => ( dispatch ) => Object.keys( actions ).reduce( (current, prop) => {
    if ( service[prop] ) {
      // when there is a service call defined for it
      // we will call the service and handle the response as part of the promise callback
      current[prop] = (...args) => service[prop]( { type: actions[prop], payload: args } ).then( dispatch );
      return current;
    }
    // when no service call is available, we will just dispatch the action
    current[prop] = (...args) => dispatch( { type: actions[prop], payload: args } );
    return current;
  }, {});

  /*-----------------*/
  /* #region actions */
  /*******************/
  const peopleActions = actions( 'people' )( 'get', 'set' );

  /*------------------*/
  /* #region reducers */
  /********************/
  const peopleReducer = ( state, action ) => {
    switch (action.type) {
      case peopleActions.set:
        return [...action.payload];
      default:
        return state;
    }
  };

  /*------------------*/
  /* #region services */
  /********************/
  const peopleData = [
    { id: 1, firstName: 'john', lastName: 'doe', gender: 'm', country: 'usa' },
    { id: 2, firstName: 'jane', lastName: 'doe', gender: 'f', country: 'usa' },
    { id: 3, firstName: 'max', lastName: 'musterman', gender: 'm', country: 'germany' },
    { id: 4, firstName: 'ulrike', lastName: 'musterman', gender: 'f', country: 'germany' },
    { id: 5, firstName: 'john', lastName: 'smith', gender: 'm', country: 'uk' },
    { id: 6, firstName: 'jane', lastName: 'smith', gender: 'f', country: 'uk' }
  ];

  const peopleService = {
    get() {
      return Promise.resolve( { type: peopleActions.set, payload: peopleData } );
    }
  };

  /*--------------------*/
  /* #region components */
  /**********************/
  const Person = ({ id, firstName, lastName, gender, country }) => {
    return (<div className="card">
      <div className="card-row"><span className="label">id:</span><span className="value">{ id }</span></div>
      <div className="card-row"><span className="label">first name:</span><span className="value">{ firstName }</span></div>
      <div className="card-row"><span className="label">last name:</span><span className="value">{ lastName }</span></div>
      <div className="card-row"><span className="label">gender:</span><span className="value">{ gender }</span></div>
      <div className="card-row"><span className="label">country:</span><span className="value">{ country }</span></div>
    </div>);
  };

  // sample component, can be seen as the default app, not connected to anything
  class Sample extends React.Component {
    getPeople() {
      this.props.get();
    }
    render() {
      let { people } = this.props;
      return (
        <div>
          <div>
            <h1>people</h1>
            <button onClick={() => this.getPeople()} type="button">Reload</button>
          </div>
          { people && <div>{ people.map( (person) => <Person {...person} key={person.id} /> ) }</div> }
        </div>
      );
    }
  };

  // higher order component
  // calls a certain action with default arguments
  // and maps the action so that the wrapped component handles it's call to the action
  // through the wrapped component
  // the wrapped component should still be able to have own props
  const withAutoload = (Component, action, ...initialActionArgs) => {
    return class extends React.Component {
      componentWillMount() {
        this.onLoad( ...initialActionArgs );
      }
      onLoad( ...functionArgs ) {
        return this.props[action]( ...functionArgs );
      }
      render() {
        // transform the props and make sure that the autoload action gets mapped through the wrapped component
        // this should ensure that their is no props discrepancy
        // and it allows the hoc to be "invisble" from the child components view
        const transformed = { ...this.props, [action]: (...childArgs) => this.onLoad(...childArgs)};
        return <Component {...transformed} />
      }
    }
  };

  /*-------------*/
  /* #region app */
  /***************/
  // quick and dirty to combine multiple reducers into the state
  const reducers = {
    people: withDefaultState( peopleReducer, [] )
  };

  // create a store, apply the promiseAware middleware
  const store = createStore( combineReducers( reducers ), applyMiddleware( promiseAware ) );

  const peopleDispatcher = serviceDispatcher( peopleActions )( peopleService );
  const peopleStateMapper = stateMapper( 'people' );

  // create a state aware sample component
  const ConnectedSample = connect( peopleStateMapper, peopleDispatcher )( withAutoload( Sample, 'get' ) );

  // render towards the container
  const target = document.querySelector('#container');
  ReactDOM.render( <Provider store={store}><ConnectedSample /></Provider>, target );
  .label {
    text-align: right;
    width: 100px;
    padding: 3px;
  }
  .value {
    text-align: left;
    padding: 3px;
  }
  .card {
    display: inline-block;
    margin: 10px;
    border: solid #a0a0a0 1px;
    background-color: #efefef;
    border-radius: 5px;
  }
  .card-row {
    margin: 3px;
  }
  .card-row > span {
    display: inline-block;
  }
<script id="react" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.2/react.js"></script>
<script id="react-dom" src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/15.6.2/react-dom.js"></script>
<script id="redux" src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.7.2/redux.js"></script>
<script id="react-redux" src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.6/react-redux.js"></script>
<script id="prop-types" src="https://cdnjs.cloudflare.com/ajax/libs/prop-types/15.6.0/prop-types.js"></script>
<script id="classnames" src="https://cdnjs.cloudflare.com/ajax/libs/classnames/2.2.5/index.js"></script>
<div id="container"></div>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.