reactmvc
Version:
M(immutable model)V(view component)C(react-router)
176 lines (155 loc) • 5.55 kB
JSX
/* eslint-disable react/no-multi-comp, import/no-unresolved */
import React, { Component, PropTypes } from 'react'
import RouterContext from 'react-router/lib/RouterContext'
function eachComponents(components, iterator) {
components.forEach(value => {
if (typeof value === 'object') {
for (const component of value) {
iterator(component)
}
} else {
iterator(value)
}
})
}
function findAsyncComponents(components) {
const asyncComponents = []
eachComponents(components, component => {
if (component.loadProps) {
asyncComponents.push(component)
}
})
return asyncComponents
}
function loadAsyncProps(components, params, location) {
const componentArray = []
const propsArray = []
const tasks = findAsyncComponents(components).map((component, index) =>
Promise.resolve().then(() => component.loadProps(params, location)).then(props => {
propsArray[index] = props
componentArray[index] = component
})
)
return Promise.all(tasks).then(() => ({ componentArray, propsArray }))
}
function lookupPropsForComponent(component, propsAndComponents) {
const { componentArray, propsArray } = propsAndComponents
const index = componentArray.indexOf(component)
return propsArray[index]
}
function createElement(component, routerProps, asyncInfo) {
if (component.loadProps) {
return (
<AsyncPropsContainer component={component} routerProps={routerProps} asyncInfo={asyncInfo} />
)
}
return React.createElement(component, routerProps)
}
class AsyncPropsContainer extends Component {
static propTypes = {
component: PropTypes.func.isRequired,
routerProps: PropTypes.object.isRequired,
asyncInfo: PropTypes.object.isRequired,
}
render() {
const { component, routerProps, asyncInfo } = this.props
const { propsAndComponents, loading, reload, routeChanged } = asyncInfo
const asyncProps = lookupPropsForComponent(component, propsAndComponents)
const props = { ...routerProps, ...asyncProps, reload, loading, routeChanged }
return React.createElement(component, props)
}
}
class AsyncProps extends Component {
static propTypes = {
components: React.PropTypes.array.isRequired,
params: React.PropTypes.object.isRequired,
location: React.PropTypes.object.isRequired,
onError: React.PropTypes.func.isRequired,
renderLoading: React.PropTypes.func.isRequired,
renderErrorPage: React.PropTypes.func.isRequired,
// server rendering
propsArray: React.PropTypes.array,
componentArray: React.PropTypes.array,
}
static defaultProps = {
onError: err => { throw err },
renderLoading: () => null,
renderErrorPage: () => null,
}
constructor(props, context) {
super(props, context)
const { propsArray, componentArray } = props
const isServerRender = propsArray && componentArray
this.state = {
loading: false,
prevProps: null,
propsAndComponents: isServerRender ? { propsArray, componentArray } : null,
routeChanged: false,
routeChangeError: false,
}
}
componentWillMount() {
const { components, params, location } = this.props
this.loadAsyncProps(components, params, location)
}
componentWillReceiveProps(nextProps) {
const routeChanged = nextProps.location.key !== this.props.location.key
if (routeChanged) {
const { components, params, location } = nextProps
this.loadAsyncProps(components, params, location)
}
}
componentWillUnmount() {
this.unmounted = true
}
loadAsyncProps(components, params, location) {
const routeChanged = location.key !== this.props.location.key
this.setState({ loading: true, prevProps: this.props, routeChanged })
const { onError } = this.props
return loadAsyncProps(components, params, location).
then(propsAndComponents => {
const sameLocation = this.props.location.key === location.key
if (sameLocation && !this.unmounted) {
this.setState({ loading: false, prevProps: null, propsAndComponents,
routeChangeError: false })
}
}).
catch(err => {
const sameLocation = this.props.location.key === location.key
const { routeChangeError, propsAndComponents } = this.state
if (sameLocation && !this.unmounted) {
this.setState({
loading: false,
routeChangeError: routeChangeError || routeChanged || !propsAndComponents,
})
}
onError(err)
})
}
reload(actionName, ...args) {
if (process.env.NODE_ENV !== 'production') {
console.info(actionName, args) // eslint-disable-line no-console
}
const { components, params, location } = this.props
return this.loadAsyncProps(components, params, location)
}
render() {
const { loading, prevProps, propsAndComponents, routeChanged, routeChangeError } = this.state
const { renderLoading, renderErrorPage } = this.props
if (routeChangeError) {
return renderErrorPage()
}
if (!propsAndComponents) {
return renderLoading()
}
const asyncInfo = { loading, routeChanged, propsAndComponents, reload: ::this.reload }
const props = prevProps || this.props
return (
<RouterContext {...props} createElement={(component, routerProps) =>
createElement(component, routerProps, asyncInfo)} />
)
}
}
export const loadPropsOnServer = ({ components, params, location }) =>
loadAsyncProps(components, params, location)
export default AsyncProps