Instantly share code, notes, and snippets.

Embed
What would you like to do?
React decorators for redux/react-router/immutable 'smart' components.

This is my typical decorator stack for a 'smart component' used as the component for react-router route. Note some code is missing here but this should give you the idea.

Example usage:

StateDetailsScene.js

import React from 'react'
import _ from 'lodash'
import route from 'core/decorators/route'
import {listActivity} from 'esa/actions/activity'
import {stateSelector} from 'esa/selectors/states'
import {statsSelector} from 'esa/selectors/stats'
import {stateLegislatorsSelector} from 'esa/selectors/legislators'
import {activityListSelector} from 'esa/selectors/activity'
import StateLayout from 'esa/components/layouts/state/StateLayout'
import SectionedView from 'esa/components/sectioned-view/SectionedView'
import Stats from 'esa/components/stats/Stats'
import Activity from 'esa/components/activity/Activity'
import Legislators from 'esa/components/legislators/Legislators'
import Loading from 'core/components/loading/Loading'
import {detailsLoading} from 'theme/styles/scenes'

@route({
  loader: ({dispatch, params}) => [
    dispatch(listActivity({query: {tags: `states:${params.stateId}`}}))
  ],
  selectors: [
    stateSelector,
    statsSelector,
    stateLegislatorsSelector,
    activityListSelector
  ],
  toJS: true
})
class StateDetailsScene extends React.Component {

  static propTypes = {
    params: React.PropTypes.object,
    state: React.PropTypes.object,
    stats: React.PropTypes.object,
    activity: React.PropTypes.object,
    legislators: React.PropTypes.object
  }

  static defaultProps = {
    activity: {}
  }

  getSections () {
    let sections = []

    if (this.props.stats) {
      sections.push({
        id: 'stats',
        title: 'Stats',
        showHeader: false,
        render: () => <Stats stats={this.props.stats}/>
      })
    }

    if (this.props.legislators) {
      sections.push({
        id: 'legislators',
        title: 'Legislators',
        showHeader: true,
        render: () => <Legislators legislators={_.values(this.props.legislators)}/>
      })
    }

    if (this.props.activity.list && this.props.activity.list.length) {
      sections.push({
        id: 'activity',
        title: 'Recent Activity',
        showHeader: true,
        render: () => <Activity activity={this.props.activity.list}/>
      })
    }

    return sections
  }

  render () {
    let sections = this.getSections()
    return this.props.state ? (
      <StateLayout state={this.props.state}>
        {sections.length ? (
          <SectionedView sections={sections}/>
        ) : (
          <Loading spinnerSize='large' fontSize={16} style={detailsLoading}/>
        )}
      </StateLayout>
    ) : null
  }
}

export default StateDetailsScene

selectors/states.js

import {createSelector} from 'reselect'

// Select a state
export const stateSelector = createSelector(
  (state) => state.states,
  (state, props) => {
    if (props.params.stateId) {
      return props.params.stateId
    }
    if (props.params.districtId) {
      return props.params.districtId.substr(0, 2)
    }
    return null
  },
  (states, stateId) => {
    return {
      state: states.getIn(['states', stateId])
    }
  }
)
export default function getDisplayName (Component) {
return Component.displayName || Component.name || 'Component'
}
import React from 'react'
import _ from 'lodash'
import getDisplayName from 'core/lib/getDisplayName'
/**
* Loaders let us fetch data in the front-end and on the server. Use this
* decorator on router components only. It MUST be the outer-most decorator
* in order for the server-side logic to find the static method this attaches.
*
* On the front-end, the loader will be run on componentDidMount.
* On the server, the loader will run before rendering.
*
* Loaders should return a promise or an array of promises.
*/
export default function loader (loader) {
return function loaderDecorator (Component) {
class LoaderComponent extends React.Component {
static displayName = `Loader(${getDisplayName(Component)})`
static WrappedComponent = Component
static loader = loader
static contextTypes = {
store: React.PropTypes.object.isRequired,
codemap: React.PropTypes.object.isRequired
}
static propTypes = {
location: React.PropTypes.object.isRequired,
params: React.PropTypes.object.isRequired
}
componentWillMount () {
if (process.env.BROWSER) {
this.runLoader()
}
}
componentDidUpdate (prevProps) {
if (!_.isEqual(prevProps.location, this.props.location)) {
this.runLoader(true)
}
}
runLoader (isUpdate = false) {
let props = this.props
let {store, codemap} = this.context
let {params, location} = this.props
let _serverRendered = store.getState().app.get('_serverRendered')
let _rendered = store.getState().app.get('_rendered')
// Only run loaders if the app has been rendered (we're not
// bootstrapping to the server-side-rendered DOM).
if (!params._loaded && (!_serverRendered || _rendered)) {
loader({
store,
dispatch: store.dispatch,
get: codemap.get,
params,
location,
props,
isUpdate
})
}
}
render () {
return (
<Component {...this.props} />
)
}
}
return LoaderComponent
}
}
import React from 'react'
import {connect} from 'react-redux'
import codemap from 'core/decorators/codemap'
import {loaderSelector} from 'core/selectors/loader'
import getDisplayName from 'core/lib/getDisplayName'
/**
* Stop updates if loaders are running.
*/
export default function loaderFreeze (Component) {
class LoaderFreezeComponent extends React.Component {
static displayName = `LoaderFreeze(${getDisplayName(Component)})`
static WrappedComponent = Component
static propTypes = {
__loader: React.PropTypes.object,
__enabled: React.PropTypes.bool
}
shouldComponentUpdate () {
return !this.props.__enabled || !this.props.__loader._loading
}
render () {
let {__loader, __enabled, ...rest} = this.props
return <Component {...rest}/>
}
}
return (
connect(loaderSelector)(
codemap({__enabled: 'enableRouteLoaders'})(
LoaderFreezeComponent
)
)
)
}
/**
* Combines @loaders, @connect, and @loaderFreeze into a convenience decorator
* to wrap a route component in.
*/
import _ from 'lodash'
import {compose} from 'redux'
import {connect} from 'react-redux'
import toJS from 'core/decorators/toJS'
import loader from 'core/decorators/loader'
import loaderFreeze from 'core/decorators/loaderFreeze'
import selectAll from 'core/lib/selectAll'
export default function route (options = {}) {
return function routeDecorator (Component) {
let stack = _.compact([
options.loader ? loader(options.loader) : null,
options.selectors ? connect(selectAll(options.selectors, true)) : null,
options.actions ? connect(null, options.actions) : null,
options.loader ? loaderFreeze : null,
options.toJS ? toJS(options.toJS) : null
])
return compose(...stack)(Component)
}
}
import _ from 'lodash'
export default function selectAll (selectors, skipWhileLoading) {
return function mapStateToProps (state, props) {
if (skipWhileLoading && state.app.getIn(['loader', '_loading'])) {
return {}
} else {
return _.reduce(selectors, (result, selector) => {
return _.extend(result, selector(state, props))
}, {})
}
}
}
import React from 'react'
import _ from 'lodash'
import getDisplayName from 'core/lib/getDisplayName'
/**
* Decorator that converts immutable props to plain JS.
* Optionally, filter which props get converted.
*/
export default function toJS (propsMap) {
function decorate (Component) {
class ToJS extends React.Component {
static displayName = `ToJS(${getDisplayName(Component)})`
static WrappedComponent = Component
toJS () {
return _.reduce(this.props, (result, value, key) => {
if (propsMap) {
if (propsMap[key]) {
result[propsMap[key]] = (typeof value.toJS === 'function') ? value.toJS() : value
} else {
result[key] = value
}
} else {
result[key] = (value && typeof value.toJS === 'function') ? value.toJS() : value
}
return result
}, {})
}
render () {
return (
<Component {...this.toJS()}/>
)
}
}
return ToJS
}
// Support `true`.
if (propsMap === true) {
propsMap = null
}
// Support an array of propNames.
if (_.isArray(propsMap)) {
propsMap = _.keyBy(propsMap, _.identity)
}
// Support using @toJS or @toJS(propsMap)
if (_.isPlainObject(propsMap) || !propsMap) {
return decorate
} else {
return decorate.apply(null, arguments)
}
}
@yairEO

This comment has been minimized.

Show comment
Hide comment
@yairEO

yairEO Sep 15, 2018

There's way too much code here for a beginner to grasp how to use a simple route decorator..
May I suggest you consider opening another gist with a much simpler codebase? maybe 2-3 files with as little code in them as possible...
Just an App.jsx and some child component and of course the decorator itself.
Thanks!

yairEO commented Sep 15, 2018

There's way too much code here for a beginner to grasp how to use a simple route decorator..
May I suggest you consider opening another gist with a much simpler codebase? maybe 2-3 files with as little code in them as possible...
Just an App.jsx and some child component and of course the decorator itself.
Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment