UNPKG

react-universal-component

Version:

A higher order component for loading components with promises

309 lines (261 loc) 7.33 kB
// @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) } } }