uptrend-redux-modules

Redux Module (redux + redux-saga + redux-saga-thunk) for requesting resources from API and storing response data into entities if provided a normalizr schema.

Downloads in past

Stats

StarsIssuesVersionUpdatedCreatedSize
uptrend-redux-modules
5140.23.05 years ago6 years agoMinified + gzip package size for uptrend-redux-modules in KB

Readme

uptrend-redux-modules

Redux Module (redux + redux-saga + redux-saga-thunk) for requesting resources from API and storing response data into entities if provided a normalizr schema.




!Build Statusbuild-badgebuild !Code Coveragecoverage-badgecoverage !versionversion-badgepackage !downloadsdownloads-badgenpmtrends !MIT Licenselicense-badgelicense
All Contributors !PRs Welcomeprs-badgeprs !Code of Conductcoc-badgecoc
!Watch on GitHubgithub-watch-badgegithub-watch !Star on GitHubgithub-star-badgegithub-star !Tweettwitter-badgetwitter

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 npmnpm which is bundled with nodenode 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

```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

```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

```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

```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

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

```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

```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

```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
redux-modules from Diego Haz.
The resource and entities modules specifically are modified version of those found in redux-modulesredux-modules and ARc.jsarc-redux-modules.

Other Solutions

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

Contributors


|
Brandon Orther

๐Ÿ’ป ๐Ÿš‡") โš ๏ธ ๐Ÿ’ก |
Dylan Foster

๐Ÿ› ๐Ÿค” | | :---: | :---: |

Thanks goes to these people (emoji keyemojis):


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

LICENSE

MIT