react-universal-component
Version:
A higher order component for loading components with promises
309 lines (261 loc) • 7.33 kB
Flow
// @flow
import React from 'react'
import PropTypes from 'prop-types'
import hoist from 'hoist-non-react-statics'
import req from './requireUniversalModule'
import type {
Config,
ConfigFunc,
ComponentOptions,
RequireAsync,
State,
Props,
Context
} from './flowTypes'
import ReportContext from './context'
import {
DefaultLoading,
DefaultError,
createDefaultRender,
isServer
} from './utils'
import { __update } from './helpers'
export { CHUNK_NAMES, MODULE_IDS } from './requireUniversalModule'
export { default as ReportChunks } from './report-chunks'
let hasBabelPlugin = false
const isHMR = () =>
// $FlowIgnore
module.hot && (module.hot.data || module.hot.status() === 'apply')
export const setHasBabelPlugin = () => {
hasBabelPlugin = true
}
export default function universal<Props: Props>(
asyncModule: Config | ConfigFunc,
opts: ComponentOptions = {}
) {
const {
render: userRender,
loading: Loading = DefaultLoading,
error: Err = DefaultError,
minDelay = 0,
alwaysDelay = false,
testBabelPlugin = false,
loadingTransition = true,
...options
} = opts
const renderFunc = userRender || createDefaultRender(Loading, Err)
const isDynamic = hasBabelPlugin || testBabelPlugin
options.isDynamic = isDynamic
options.usesBabelPlugin = hasBabelPlugin
options.modCache = {}
options.promCache = {}
return class UniversalComponent extends React.Component<void, Props, *> {
/* eslint-disable react/sort-comp */
_initialized: boolean
_asyncOnly: boolean
state: State
props: Props
/* eslint-enable react/sort-comp */
static contextType = ReportContext
static preload(props: Props) {
props = props || {}
const { requireAsync, requireSync } = req(asyncModule, options, props)
let mod
try {
mod = requireSync(props)
}
catch (error) {
return Promise.reject(error)
}
return Promise.resolve()
.then(() => {
if (mod) return mod
return requireAsync(props)
})
.then(mod => {
hoist(UniversalComponent, mod, {
preload: true,
preloadWeak: true
})
return mod
})
}
static preloadWeak(props: Props) {
props = props || {}
const { requireSync } = req(asyncModule, options, props)
const mod = requireSync(props)
if (mod) {
hoist(UniversalComponent, mod, {
preload: true,
preloadWeak: true
})
}
return mod
}
requireAsyncInner(
requireAsync: RequireAsync,
props: Props,
state: State,
isMount?: boolean
) {
if (!state.mod && loadingTransition) {
this.update({ mod: null, props }) // display `loading` during componentWillReceiveProps
}
const time = new Date()
requireAsync(props)
.then((mod: ?any) => {
const state = { mod, props }
const timeLapsed = new Date() - time
if (timeLapsed < minDelay) {
const extraDelay = minDelay - timeLapsed
return setTimeout(() => this.update(state, isMount), extraDelay)
}
this.update(state, isMount)
})
.catch(error => this.update({ error, props }))
}
update = (
state: State,
isMount?: boolean = false,
isSync?: boolean = false,
isServer?: boolean = false
) => {
if (!this._initialized) return
if (!state.error) state.error = null
this.handleAfter(state, isMount, isSync, isServer)
}
handleBefore(
isMount: boolean,
isSync: boolean,
isServer?: boolean = false
) {
if (this.props.onBefore) {
const { onBefore } = this.props
const info = { isMount, isSync, isServer }
onBefore(info)
}
}
handleAfter(
state: State,
isMount: boolean,
isSync: boolean,
isServer: boolean
) {
const { mod, error } = state
if (mod && !error) {
hoist(UniversalComponent, mod, {
preload: true,
preloadWeak: true
})
if (this.props.onAfter) {
const { onAfter } = this.props
const info = { isMount, isSync, isServer }
onAfter(info, mod)
}
}
else if (error && this.props.onError) {
this.props.onError(error)
}
this.setState(state)
}
// $FlowFixMe
init(props) {
const { addModule, requireSync, requireAsync, asyncOnly } = req(
asyncModule,
options,
props
)
let mod
try {
mod = requireSync(props)
}
catch (error) {
return __update(props, { error, props }, this._initialized)
}
this._asyncOnly = asyncOnly
const chunkName = addModule(props) // record the module for SSR flushing :)
if (this.context && this.context.report) {
this.context.report(chunkName)
}
if (mod || isServer) {
this.handleBefore(true, true, isServer)
return __update(
props,
{ asyncOnly, props, mod },
this._initialized,
true,
true,
isServer
)
}
this.handleBefore(true, false)
this.requireAsyncInner(
requireAsync,
props,
{ props, asyncOnly, mod },
true
)
return { mod, asyncOnly, props }
}
constructor(props: Props, context: Context) {
super(props, context)
this.state = this.init(this.props)
// $FlowFixMe
this.state.error = null
}
static getDerivedStateFromProps(nextProps, currentState) {
const { requireSync, shouldUpdate } = req(
asyncModule,
options,
nextProps,
currentState.props
)
if (isHMR() && shouldUpdate(currentState.props, nextProps)) {
const mod = requireSync(nextProps)
return { ...currentState, mod }
}
return null
}
componentDidMount() {
this._initialized = true
}
componentDidUpdate(prevProps: Props) {
if (isDynamic || this._asyncOnly) {
const { requireSync, requireAsync, shouldUpdate } = req(
asyncModule,
options,
this.props,
prevProps
)
if (shouldUpdate(this.props, prevProps)) {
let mod
try {
mod = requireSync(this.props)
}
catch (error) {
return this.update({ error })
}
this.handleBefore(false, !!mod)
if (!mod) {
return this.requireAsyncInner(requireAsync, this.props, { mod })
}
const state = { mod }
if (alwaysDelay) {
if (loadingTransition) this.update({ mod: null }) // display `loading` during componentWillReceiveProps
setTimeout(() => this.update(state, false, true), minDelay)
return
}
this.update(state, false, true)
}
}
}
componentWillUnmount() {
this._initialized = false
}
render() {
const { isLoading, error: userError, ...props } = this.props
const { mod, error } = this.state
return renderFunc(props, mod, isLoading, userError || error)
}
}
}