0.23.0 โ€ข Published 5 years ago

uptrend-redux-modules v0.23.0

Weekly downloads
52
License
MIT
Repository
github
Last release
5 years ago

Build Status Code Coverage version downloads MIT License

All Contributors PRs Welcome Code of Conduct

Watch on GitHub Star on GitHub Tweet

The problem

At Uptrend we enjoy building React applications and have had success using redux + normalizr to manage state and redux-saga + redux-saga-thunk to orchestrate application side effects (i.e. asynchronous things like data fetching). Code is easy to understand and typically works as expected but someone could have a criticism about the amount of ceremony and boilerplate required.

Typically, whenever adding a new entity to an appย it required us to write reducers, actions, sagas, schemas, selectors, and container components to get basic CRUD functionality.

This solution

Create a concise and straightforward way to make HTTP requests that normalize response handling including normalization of response data into index entities in the redux store. To get CRUD functionality for a new entity, you add a normalizr schema and use the provided actions and selectors provided by URM (uptrend-redux-modules). URM also provides render prop React components that simplify and reduce the amount of code needed.

Below are code examples to highlight what using URM resource and entities looks like:

URM Resource & Entities Graph

ResourceDetailLoader Component

const OrgDetailAutoLoader = ({orgId}) => (
  <ResourceDetailLoader resource="org" resourceId={orgId} autoLoad>
    {({status, result, onEventLoadResource}) => (
      <div>
        <pre>{'autoLoad={true}'}</pre>

        <button onClick={onEventLoadResource} disabled={status.loading}>
          Load Resource
        </button>

        {status.initial && <span className="label label-default">initial</span>}
        {status.loading && <span className="label label-primary">loading</span>}
        {status.error && <span className="label label-danger">error</span>}
        {status.success && <span className="label label-success">success</span>}

        {status.loading ? (
          <h5>Loading...</h5>
        ) : (
          result && (
            <div>
              <div>
                Org ID: <code>{result.id}</code>
              </div>
              <div>
                Active: <code>{result.active ? 'Yes' : 'No'}</code>
              </div>
            </div>
          )
        )}
      </div>
    )}
  </ResourceDetailLoader>
)

ResourceListLoader Component

const OrgListLoader = () => (
  <ResourceListLoader resource="org">
    {({status, result, onEventLoadResource}) => (
      <div>
        <div>
          <pre>{'autoLoad={false}'}</pre>
          <pre>{JSON.stringify(status, null, 2)}</pre>
        </div>

        <button onClick={onEventLoadResource} disabled={status.loading}>
          Load Resource
        </button>

        {status.initial && <span className="label label-default">initial</span>}
        {status.loading && <span className="label label-primary">loading</span>}
        {status.error && <span className="label label-danger">error</span>}
        {status.success && <span className="label label-success">success</span>}

        {status.loading ? (
          <h5>Loading...</h5>
        ) : (
          result &&
          result.map(org => (
            <div key={org.id}>
              <span>
                Org ID: <code>{org.id}</code>
              </span>
              <span>
                Active: <code>{org.active ? 'Yes' : 'No'}</code>
              </span>
            </div>
          ))
        )}
      </div>
    )}
  </ResourceListLoader>
)

Using Resource Redux-Saga-Thunk Style

Resource actions provide a promise based interface that redux-saga-thunk allows. Below shows how a resource can use without using selectors. This is nice when you need resource data to save locally.

import React from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'

const mapDispatchToProps = dispatch => ({
  loadGroups: () =>
    dispatch(resourceListReadRequest('group', {active: true}, 'group')),
})

class GroupListContainer extends React.Component {
  state = {
    loading: false,
    groupList: null,
  }

  componentDidMount() {
    this.loadGroups()
  }

  loadGroups() {
    this.setState({loading: true})
    this.props.loadGroups().then(this.handleLoadSuccess, this.handleLoadFail)
  }

  handleLoadFail = error => {
    this.setState({loading: false, error})
  }

  handleLoadSuccess = ({entities}) => {
    this.setState({loading: false, groupList: entities})
  }

  render() {
    const {loading, groupList} = this.state

    if (loading) return <div>Loading...</div>

    return (
      <ul>{groupList.map(group => <li key={group.id}>{group.name}</li>)}</ul>
    )
  }
}

GroupListContainer.propTypes = {
  fetchTripGroupList: PropTypes.func.isRequired,
}

export default connect(null, mapDispatchToProps)(GroupListContainer)

Redux Modules

There

// TODO

Table of Contents

Installation

This module is distributed via npm which is bundled with node and should be installed as one of your project's dependencies:

yarn add uptrend-redux-modules

Example Project Usage

Below is an example of how one may set it up in a react app using the resource and entities redux-modules.

Do note there are many ways you could organize your project and this example is not strict guidelines by any means.

Resource & Entities

  • src/store/modules/resource/index.js

    // - src/store/modules/resource/index.js
    import {createResource} from 'uptrend-redux-modules'
    
    // createResource(...) => { actions, reducers, sagas, selectors }
    export default createResource()
  • src/store/modules/entities/index.js

    // - src/store/modules/entities/index.js
    import {createEntities} from 'uptrend-redux-modules'
    import schemas from './schemas'
    
    // createEntities(...) => { actions, middleware, reducers, sagas, selectors }
    export default createEntities({schemas})
  • src/store/modules/entities/schema.js

    // - src/store/modules/entities/schemas.js
    import {schema} from 'normalizr'
    
    export const user = new schema.Entity('users')
    export const team = new schema.Entity('teams', {owner: user, members: [user]})
  • src/store/actions.js

    // - src/store/actions.js
    import {actions as entities} from 'src/store/modules/entities';
    import {actions as resource} from 'src/store/modules/resource';
    
    export {
      ...entities,
      ...resource,
    }
  • src/store/middlewares.js

    // - src/store/middlewares.js
    import {middleware as entities} from 'src/store/modules/entities'
    
    export default [
      // redux-modules middlewares
      entities,
    ]
  • src/store/reducers.js

    // - src/store/reducer.js
    import {combineReducers} from 'redux'
    
    import {reducer as entities} from 'src/store/modules/entities'
    import {reducer as resource} from 'src/store/modules/resource'
    
    export default combineReducers({
      entities,
      resource,
    })
  • src/store/sagas.js

    // - src/store/sagas.js
    import {sagas as entities} from 'src/store/modules/entities'
    import {sagas as resource} from 'src/store/modules/resource'
    
    // single entry point to start all Sagas at once
    export default function*(services = {}) {
      try {
        yield all([
          // app specific sagas
          example(services),
    
          // redux-modules sagas
          entities(services),
          resource(services),
        ])
      } catch (error) {
        console.error('ROOT SAGA ERROR!!!', error)
        console.trace()
      }
    }
  • src/store/selectors.js

    // - src/store/selectors.js
    import {selectors as fromEntities} from 'src/store/modules/entities'
    import {selectors as fromResource} from 'src/store/modules/resource'
    
    export {fromEntities, fromResource}

Usage

// TODO

Inspiration

Organizing actions, reducers, selectors, sagas, etc. into a module is based on redux-modules from Diego Haz.

The resource and entities modules specifically are modified version of those found in redux-modules and ARc.js.

Other Solutions

I'm not aware of any, if you are please make a pull request and add it here!

Contributors

Brandon Orther๐Ÿ’ป ๐Ÿš‡ โš ๏ธ ๐Ÿ’กDylan Foster๐Ÿ› ๐Ÿค”

Thanks goes to these people (emoji key):

This project follows the all-contributors specification. Contributions of any kind welcome!

LICENSE

MIT

0.23.0

5 years ago

0.22.0

5 years ago

0.21.0

5 years ago

0.20.0

5 years ago

0.19.0

6 years ago

0.18.0

6 years ago

0.17.0

6 years ago

0.16.2

6 years ago

0.16.1

6 years ago

0.16.0

6 years ago

0.15.0

6 years ago

0.14.0

6 years ago

0.13.0

6 years ago

0.12.0

6 years ago

0.11.1

6 years ago

0.11.0

6 years ago

0.10.0

6 years ago

0.9.0

6 years ago

0.8.0

6 years ago

0.7.1

6 years ago

0.7.0

6 years ago

0.6.7

6 years ago

0.6.6

6 years ago

0.6.5

6 years ago

0.6.4

6 years ago

0.6.3

6 years ago

0.6.2

6 years ago

0.6.1

6 years ago

0.6.0

6 years ago

0.5.0

6 years ago

0.4.1

6 years ago

0.4.0

6 years ago

0.3.0

6 years ago

0.2.0

6 years ago