Skip to content

Commit

Permalink
Support Concurrent Mode in Loadable (vercel#9026)
Browse files Browse the repository at this point in the history
  • Loading branch information
devknoll authored and timneutkens committed Oct 18, 2019
1 parent b7efb3f commit d28e46a
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 86 deletions.
4 changes: 2 additions & 2 deletions packages/next/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ export default async function getBaseWebpackConfig(
react: {
name: 'commons',
chunks: 'all',
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
test: /[\\/]node_modules[\\/](react|react-dom|scheduler|use-subscription)[\\/]/,
},
},
},
Expand All @@ -237,7 +237,7 @@ export default async function getBaseWebpackConfig(
// This regex ignores nested copies of framework libraries so they're
// bundled with their issuer.
// https://github.com/zeit/next.js/pull/9012
test: /(?<!node_modules.*)[\\/]node_modules[\\/](react|react-dom|scheduler|prop-types)[\\/]/,
test: /(?<!node_modules.*)[\\/]node_modules[\\/](react|react-dom|scheduler|prop-types|use-subscription)[\\/]/,
priority: 40,
},
lib: {
Expand Down
190 changes: 106 additions & 84 deletions packages/next/next-server/lib/loadable.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
// Modified to be compatible with webpack 4 / Next.js

import React from 'react'
import { useSubscription } from 'use-subscription'
import { LoadableContext } from './loadable-context'

const ALL_INITIALIZERS = []
Expand Down Expand Up @@ -121,13 +122,19 @@ function createLoadableComponent (loadFn, options) {
options
)

let res = null
let subscription = null

function init () {
if (!res) {
res = loadFn(opts.loader)
if (!subscription) {
const sub = new LoadableSubscription(loadFn, opts)
subscription = {
getCurrentValue: sub.getCurrentValue.bind(sub),
subscribe: sub.subscribe.bind(sub),
retry: sub.retry.bind(sub),
promise: sub.promise.bind(sub)
}
}
return res.promise
return subscription.promise()
}

// Server only
Expand All @@ -151,113 +158,128 @@ function createLoadableComponent (loadFn, options) {
})
}

return class LoadableComponent extends React.Component {
constructor (props) {
super(props)
init()

this.state = {
error: res.error,
pastDelay: false,
timedOut: false,
loading: res.loading,
loaded: res.loaded
}
}
const LoadableComponent = (props, ref) => {
init()

const context = React.useContext(LoadableContext)
const state = useSubscription(subscription)

React.useImperativeHandle(ref, () => ({
retry: subscription.retry
}))

static preload () {
return init()
if (context && Array.isArray(opts.modules)) {
opts.modules.forEach(moduleName => {
context(moduleName)
})
}

static contextType = LoadableContext
// TODO: change it before next major React release
// eslint-disable-next-line
UNSAFE_componentWillMount() {
this._mounted = true
this._loadModule()
if (state.loading || state.error) {
return React.createElement(opts.loading, {
isLoading: state.loading,
pastDelay: state.pastDelay,
timedOut: state.timedOut,
error: state.error,
retry: subscription.retry
})
} else if (state.loaded) {
return opts.render(state.loaded, props)
} else {
return null
}
}

_loadModule () {
if (this.context && Array.isArray(opts.modules)) {
opts.modules.forEach(moduleName => {
this.context(moduleName)
})
}
LoadableComponent.preload = () => init()
LoadableComponent.displayName = 'LoadableComponent'

if (!res.loading) {
return
}
return React.forwardRef(LoadableComponent)
}

class LoadableSubscription {
constructor (loadFn, opts) {
this._loadFn = loadFn
this._opts = opts
this._callbacks = new Set()
this._delay = null
this._timeout = null

this.retry()
}

promise () {
return this._res.promise
}

retry () {
this._clearTimeouts()
this._res = this._loadFn(this._opts.loader)

this._state = {
pastDelay: false,
timedOut: false
}

const { _res: res, _opts: opts } = this

if (res.loading) {
if (typeof opts.delay === 'number') {
if (opts.delay === 0) {
this.setState({ pastDelay: true })
this._state.pastDelay = true
} else {
this._delay = setTimeout(() => {
this.setState({ pastDelay: true })
this._update({
pastDelay: true
})
}, opts.delay)
}
}

if (typeof opts.timeout === 'number') {
this._timeout = setTimeout(() => {
this.setState({ timedOut: true })
this._update({ timedOut: true })
}, opts.timeout)
}
}

let update = () => {
if (!this._mounted) {
return
}

this.setState({
error: res.error,
loaded: res.loaded,
loading: res.loading
})

this._res.promise
.then(() => {
this._update()
this._clearTimeouts()
}

res.promise
.then(() => {
update()
})
// eslint-disable-next-line handle-callback-err
.catch(err => {
update()
})
}
})
// eslint-disable-next-line handle-callback-err
.catch(err => {
this._update()
this._clearTimeouts()
})
this._update({})
}

componentWillUnmount () {
this._mounted = false
this._clearTimeouts()
_update (partial) {
this._state = {
...this._state,
...partial
}
this._callbacks.forEach(callback => callback())
}

_clearTimeouts () {
clearTimeout(this._delay)
clearTimeout(this._timeout)
}
_clearTimeouts () {
clearTimeout(this._delay)
clearTimeout(this._timeout)
}

retry = () => {
this.setState({ error: null, loading: true, timedOut: false })
res = loadFn(opts.loader)
this._loadModule()
getCurrentValue () {
return {
...this._state,
error: this._res.error,
loaded: this._res.loaded,
loading: this._res.loading
}
}

render () {
if (this.state.loading || this.state.error) {
return React.createElement(opts.loading, {
isLoading: this.state.loading,
pastDelay: this.state.pastDelay,
timedOut: this.state.timedOut,
error: this.state.error,
retry: this.retry
})
} else if (this.state.loaded) {
return opts.render(this.state.loaded, this.props)
} else {
return null
}
subscribe (callback) {
this._callbacks.add(callback)
return () => {
this._callbacks.delete(callback)
}
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
"terser": "4.0.0",
"unfetch": "4.1.0",
"url": "0.11.0",
"use-subscription": "1.1.1",
"watchpack": "2.0.0-beta.5",
"webpack": "4.39.0",
"webpack-dev-middleware": "3.7.0",
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -14577,6 +14577,11 @@ [email protected], url@^0.11.0:
punycode "1.3.2"
querystring "0.2.0"

[email protected]:
version "1.1.1"
resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.1.1.tgz#5509363e9bb152c4fb334151d4dceb943beaa7bb"
integrity sha512-gk4fPTYvNhs6Ia7u8/+K7bM7sZ7O7AMfWtS+zPO8luH+zWuiGgGcrW0hL4MRWZSzXo+4ofNorf87wZwBKz2YdQ==

use@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
Expand Down

0 comments on commit d28e46a

Please sign in to comment.