uptrend-redux-modules
v0.23.0
Published
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
27
Readme
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:
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