diff --git a/.gitignore b/.gitignore index 2131e15..90d188c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules *.log .idea .DS_Store +.history diff --git a/package.json b/package.json index 341555e..57dc98c 100644 --- a/package.json +++ b/package.json @@ -65,8 +65,8 @@ "jest": "^20.0.4", "lint-staged": "^7.2.0", "prettier": "^1.3.1", - "react": "^16.4.1", - "react-test-renderer": "^16.4.1", + "react": "^16.4.2", + "react-test-renderer": "^16.4.2", "rimraf": "^2.5.4", "semantic-release": "^6.3.6", "slash": "^1.0.0", @@ -93,6 +93,7 @@ }, "dependencies": { "hoist-non-react-statics": "^2.2.1", - "prop-types": "^15.5.10" + "prop-types": "^15.5.10", + "react-hot-loader": "^4.3.6" } } diff --git a/src/helpers.js b/src/helpers.js new file mode 100644 index 0000000..61e82e0 --- /dev/null +++ b/src/helpers.js @@ -0,0 +1,40 @@ +import hoist from 'hoist-non-react-statics' +import UniversalComponent from './index' + +export const __update = ( + props, + state, + isInitialized, + isMount = false, + isSync = false, + isServer = false +) => { + if (!isInitialized) return state + if (!state.error) { + state.error = null + } + return __handleAfter(props, state, isMount, isSync, isServer) +} + +/* eslint class-methods-use-this: ["error", { "exceptMethods": ["__handleAfter"] }] */ +export const __handleAfter = (props, state, isMount, isSync, isServer) => { + const { mod, error } = state + + if (mod && !error) { + hoist(UniversalComponent, mod, { + preload: true, + preloadWeak: true + }) + + if (props.onAfter) { + const { onAfter } = props + const info = { isMount, isSync, isServer } + onAfter(info, mod) + } + } + else if (error && props.onError) { + props.onError(error) + } + + return state +} diff --git a/src/index.js b/src/index.js index 025b575..96135d6 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import hoist from 'hoist-non-react-statics' +import { Context } from 'vm' import req from './requireUniversalModule' import type { @@ -19,6 +20,7 @@ import { createDefaultRender, isServer } from './utils' +import { __update } from './helpers' export { CHUNK_NAMES, MODULE_IDS } from './requireUniversalModule' export { default as ReportChunks } from './report-chunks' @@ -58,7 +60,7 @@ export default function universal( return class UniversalComponent extends React.Component { /* eslint-disable react/sort-comp */ - _mounted: boolean + _initialized: boolean _asyncOnly: boolean state: State @@ -112,103 +114,22 @@ export default function universal( report: PropTypes.func } - constructor(props: Props, context: {}) { - super(props, context) - this.state = { error: null } - } - - componentWillMount() { - this._mounted = true - - const { addModule, requireSync, requireAsync, asyncOnly } = req( - asyncModule, - options, - this.props - ) - - let mod - - try { - mod = requireSync(this.props, this.context) - } - catch (error) { - return this.update({ error }) - } - - this._asyncOnly = asyncOnly - const chunkName = addModule(this.props) // record the module for SSR flushing :) - - if (this.context.report) { - this.context.report(chunkName) - } - - if (mod || isServer) { - this.handleBefore(true, true, isServer) - this.update({ mod }, true, true, isServer) - return - } - - this.handleBefore(true, false) - this.requireAsync(requireAsync, this.props, true) - } - - componentWillUnmount() { - this._mounted = false - } - - componentWillReceiveProps(nextProps: Props) { - if (isDynamic || this._asyncOnly) { - const { requireSync, requireAsync, shouldUpdate } = req( - asyncModule, - options, - nextProps, - this.props - ) - - if (shouldUpdate(nextProps, this.props)) { - let mod - - try { - mod = requireSync(nextProps, this.context) - } - catch (error) { - return this.update({ error }) - } - - this.handleBefore(false, !!mod) - - if (!mod) { - return this.requireAsync(requireAsync, nextProps) - } - - 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) - } - else if (isHMR()) { - const mod = requireSync(nextProps, this.context) - this.setState({ mod: () => null }) // HMR /w Redux and HOCs can be finicky, so we - setTimeout(() => this.setState({ mod })) // toggle components to insure updates occur - } - } - } - - requireAsync(requireAsync: RequireAsync, props: Props, isMount?: boolean) { - if (this.state.mod && loadingTransition) { - this.update({ mod: null }) // display `loading` during componentWillReceiveProps + requireAsyncInner( + requireAsync: RequireAsync, + props: Props, + state: State, + context: Context, + isMount?: boolean + ) { + if (!state.mod && loadingTransition) { + this.update({ mod: null, props }) // display `loading` during componentWillReceiveProps } const time = new Date() - requireAsync(props, this.context) + requireAsync(props, context) .then((mod: ?any) => { - const state = { mod } + const state = { mod, props, context } const timeLapsed = new Date() - time if (timeLapsed < minDelay) { @@ -218,7 +139,7 @@ export default function universal( this.update(state, isMount) }) - .catch(error => this.update({ error })) + .catch(error => this.update({ error, props, context })) } update = ( @@ -227,7 +148,7 @@ export default function universal( isSync?: boolean = false, isServer?: boolean = false ) => { - if (!this._mounted) return + if (!this._initialized) return if (!state.error) state.error = null this.handleAfter(state, isMount, isSync, isServer) @@ -272,6 +193,118 @@ export default function universal( this.setState(state) } + init(props, context) { + const { addModule, requireSync, requireAsync, asyncOnly } = req( + asyncModule, + options, + props + ) + + let mod + + try { + mod = requireSync(props, context) + } + catch (error) { + return __update(props, { error, props, context }, this._initialized) + } + + this._asyncOnly = asyncOnly + const chunkName = addModule(props) // record the module for SSR flushing :) + + if (context.report) { + context.report(chunkName) + } + + if (mod || isServer) { + this.handleBefore(true, true, isServer) + return __update( + props, + { asyncOnly, props, mod, context }, + this._initialized, + true, + true, + isServer + ) + } + + this.handleBefore(true, false) + this.requireAsyncInner( + requireAsync, + props, + { props, asyncOnly, mod, context }, + context, + true + ) + return { mod, asyncOnly, context, props } + } + + constructor(props: Props, context: {}) { + super(props, context) + this.state = this.init(this.props, this.context) + 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, currentState.context) + return { ...currentState, mod } + } + return null + } + + componentDidMount() { + this._initialized = true + } + + componentDidUpdate(prevProps) { + 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, this.context) + } + 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 diff --git a/yarn.lock b/yarn.lock index 57adf54..0337635 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1818,6 +1818,10 @@ doctrine@^2.0.0, doctrine@^2.1.0: dependencies: esutils "^2.0.2" +dom-walk@^0.1.0: + version "0.1.1" + resolved "http://0.0.0.0:4873/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018" + dot-prop@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" @@ -2267,7 +2271,7 @@ fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" -fast-levenshtein@~2.0.4: +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" @@ -2698,6 +2702,13 @@ global-prefix@^0.1.4: is-windows "^0.2.0" which "^1.2.12" +global@^4.3.0: + version "4.3.2" + resolved "http://0.0.0.0:4873/global/-/global-4.3.2.tgz#e76989268a6c74c38908b1305b10fc0e394e9d0f" + dependencies: + min-document "^2.19.0" + process "~0.5.1" + globals@^9.14.0, globals@^9.18.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" @@ -2835,6 +2846,10 @@ hoist-non-react-statics@^2.2.1: version "2.5.4" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.4.tgz#fc3b1ac05d2ae3abedec84eba846511b0d4fcc4f" +hoist-non-react-statics@^2.5.0: + version "2.5.5" + resolved "http://0.0.0.0:4873/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" + home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" @@ -4206,6 +4221,12 @@ mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" +min-document@^2.19.0: + version "2.19.0" + resolved "http://0.0.0.0:4873/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" + dependencies: + dom-walk "^0.1.0" + minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -4910,6 +4931,10 @@ process@^0.11.1: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" +process@~0.5.1: + version "0.5.2" + resolved "http://0.0.0.0:4873/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf" + progress@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" @@ -4999,22 +5024,43 @@ rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-is@^16.4.1: - version "16.4.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.1.tgz#d624c4650d2c65dbd52c72622bbf389435d9776e" +<<<<<<< HEAD +react-hot-loader@^4.3.6: + version "4.3.6" + resolved "http://0.0.0.0:4873/react-hot-loader/-/react-hot-loader-4.3.6.tgz#26e1491f08daf2bad99d141b1927c9faadef2fb4" + dependencies: + fast-levenshtein "^2.0.6" + global "^4.3.0" + hoist-non-react-statics "^2.5.0" + prop-types "^15.6.1" + react-lifecycles-compat "^3.0.4" + shallowequal "^1.0.2" + +======= +>>>>>>> 8a3bd2b7f1685c04fb5057571f3b3cfa83f3f185 +react-is@^16.4.2: + version "16.4.2" + resolved "http://0.0.0.0:4873/react-is/-/react-is-16.4.2.tgz#84891b56c2b6d9efdee577cc83501dfc5ecead88" -react-test-renderer@^16.4.1: - version "16.4.1" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.4.1.tgz#f2fb30c2c7b517db6e5b10ed20bb6b0a7ccd8d70" +<<<<<<< HEAD +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "http://0.0.0.0:4873/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + +======= +>>>>>>> 8a3bd2b7f1685c04fb5057571f3b3cfa83f3f185 +react-test-renderer@^16.4.2: + version "16.4.2" + resolved "http://0.0.0.0:4873/react-test-renderer/-/react-test-renderer-16.4.2.tgz#4e03eca9359bb3210d4373f7547d1364218ef74e" dependencies: fbjs "^0.8.16" object-assign "^4.1.1" prop-types "^15.6.0" - react-is "^16.4.1" + react-is "^16.4.2" -react@^16.4.1: - version "16.4.1" - resolved "https://registry.yarnpkg.com/react/-/react-16.4.1.tgz#de51ba5764b5dbcd1f9079037b862bd26b82fe32" +react@^16.4.2: + version "16.4.2" + resolved "http://0.0.0.0:4873/react/-/react-16.4.2.tgz#2cd90154e3a9d9dd8da2991149fdca3c260e129f" dependencies: fbjs "^0.8.16" loose-envify "^1.1.0" @@ -5498,6 +5544,10 @@ shallow-clone@^0.1.2: lazy-cache "^0.2.3" mixin-object "^2.0.1" +shallowequal@^1.0.2: + version "1.1.0" + resolved "http://0.0.0.0:4873/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"