Create a gist now

Instantly share code, notes, and snippets.

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)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment