Skip to content

Commit d28e46a

Browse files
devknolltimneutkens
authored andcommitted
Support Concurrent Mode in Loadable (#9026)
1 parent b7efb3f commit d28e46a

File tree

4 files changed

+114
-86
lines changed

4 files changed

+114
-86
lines changed

packages/next/build/webpack-config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ export default async function getBaseWebpackConfig(
220220
react: {
221221
name: 'commons',
222222
chunks: 'all',
223-
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
223+
test: /[\\/]node_modules[\\/](react|react-dom|scheduler|use-subscription)[\\/]/,
224224
},
225225
},
226226
},
@@ -237,7 +237,7 @@ export default async function getBaseWebpackConfig(
237237
// This regex ignores nested copies of framework libraries so they're
238238
// bundled with their issuer.
239239
// https://github.com/zeit/next.js/pull/9012
240-
test: /(?<!node_modules.*)[\\/]node_modules[\\/](react|react-dom|scheduler|prop-types)[\\/]/,
240+
test: /(?<!node_modules.*)[\\/]node_modules[\\/](react|react-dom|scheduler|prop-types|use-subscription)[\\/]/,
241241
priority: 40,
242242
},
243243
lib: {

packages/next/next-server/lib/loadable.js

Lines changed: 106 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
2222
// Modified to be compatible with webpack 4 / Next.js
2323

2424
import React from 'react'
25+
import { useSubscription } from 'use-subscription'
2526
import { LoadableContext } from './loadable-context'
2627

2728
const ALL_INITIALIZERS = []
@@ -121,13 +122,19 @@ function createLoadableComponent (loadFn, options) {
121122
options
122123
)
123124

124-
let res = null
125+
let subscription = null
125126

126127
function init () {
127-
if (!res) {
128-
res = loadFn(opts.loader)
128+
if (!subscription) {
129+
const sub = new LoadableSubscription(loadFn, opts)
130+
subscription = {
131+
getCurrentValue: sub.getCurrentValue.bind(sub),
132+
subscribe: sub.subscribe.bind(sub),
133+
retry: sub.retry.bind(sub),
134+
promise: sub.promise.bind(sub)
135+
}
129136
}
130-
return res.promise
137+
return subscription.promise()
131138
}
132139

133140
// Server only
@@ -151,113 +158,128 @@ function createLoadableComponent (loadFn, options) {
151158
})
152159
}
153160

154-
return class LoadableComponent extends React.Component {
155-
constructor (props) {
156-
super(props)
157-
init()
158-
159-
this.state = {
160-
error: res.error,
161-
pastDelay: false,
162-
timedOut: false,
163-
loading: res.loading,
164-
loaded: res.loaded
165-
}
166-
}
161+
const LoadableComponent = (props, ref) => {
162+
init()
163+
164+
const context = React.useContext(LoadableContext)
165+
const state = useSubscription(subscription)
166+
167+
React.useImperativeHandle(ref, () => ({
168+
retry: subscription.retry
169+
}))
167170

168-
static preload () {
169-
return init()
171+
if (context && Array.isArray(opts.modules)) {
172+
opts.modules.forEach(moduleName => {
173+
context(moduleName)
174+
})
170175
}
171176

172-
static contextType = LoadableContext
173-
// TODO: change it before next major React release
174-
// eslint-disable-next-line
175-
UNSAFE_componentWillMount() {
176-
this._mounted = true
177-
this._loadModule()
177+
if (state.loading || state.error) {
178+
return React.createElement(opts.loading, {
179+
isLoading: state.loading,
180+
pastDelay: state.pastDelay,
181+
timedOut: state.timedOut,
182+
error: state.error,
183+
retry: subscription.retry
184+
})
185+
} else if (state.loaded) {
186+
return opts.render(state.loaded, props)
187+
} else {
188+
return null
178189
}
190+
}
179191

180-
_loadModule () {
181-
if (this.context && Array.isArray(opts.modules)) {
182-
opts.modules.forEach(moduleName => {
183-
this.context(moduleName)
184-
})
185-
}
192+
LoadableComponent.preload = () => init()
193+
LoadableComponent.displayName = 'LoadableComponent'
186194

187-
if (!res.loading) {
188-
return
189-
}
195+
return React.forwardRef(LoadableComponent)
196+
}
190197

198+
class LoadableSubscription {
199+
constructor (loadFn, opts) {
200+
this._loadFn = loadFn
201+
this._opts = opts
202+
this._callbacks = new Set()
203+
this._delay = null
204+
this._timeout = null
205+
206+
this.retry()
207+
}
208+
209+
promise () {
210+
return this._res.promise
211+
}
212+
213+
retry () {
214+
this._clearTimeouts()
215+
this._res = this._loadFn(this._opts.loader)
216+
217+
this._state = {
218+
pastDelay: false,
219+
timedOut: false
220+
}
221+
222+
const { _res: res, _opts: opts } = this
223+
224+
if (res.loading) {
191225
if (typeof opts.delay === 'number') {
192226
if (opts.delay === 0) {
193-
this.setState({ pastDelay: true })
227+
this._state.pastDelay = true
194228
} else {
195229
this._delay = setTimeout(() => {
196-
this.setState({ pastDelay: true })
230+
this._update({
231+
pastDelay: true
232+
})
197233
}, opts.delay)
198234
}
199235
}
200236

201237
if (typeof opts.timeout === 'number') {
202238
this._timeout = setTimeout(() => {
203-
this.setState({ timedOut: true })
239+
this._update({ timedOut: true })
204240
}, opts.timeout)
205241
}
242+
}
206243

207-
let update = () => {
208-
if (!this._mounted) {
209-
return
210-
}
211-
212-
this.setState({
213-
error: res.error,
214-
loaded: res.loaded,
215-
loading: res.loading
216-
})
217-
244+
this._res.promise
245+
.then(() => {
246+
this._update()
218247
this._clearTimeouts()
219-
}
220-
221-
res.promise
222-
.then(() => {
223-
update()
224-
})
225-
// eslint-disable-next-line handle-callback-err
226-
.catch(err => {
227-
update()
228-
})
229-
}
248+
})
249+
// eslint-disable-next-line handle-callback-err
250+
.catch(err => {
251+
this._update()
252+
this._clearTimeouts()
253+
})
254+
this._update({})
255+
}
230256

231-
componentWillUnmount () {
232-
this._mounted = false
233-
this._clearTimeouts()
257+
_update (partial) {
258+
this._state = {
259+
...this._state,
260+
...partial
234261
}
262+
this._callbacks.forEach(callback => callback())
263+
}
235264

236-
_clearTimeouts () {
237-
clearTimeout(this._delay)
238-
clearTimeout(this._timeout)
239-
}
265+
_clearTimeouts () {
266+
clearTimeout(this._delay)
267+
clearTimeout(this._timeout)
268+
}
240269

241-
retry = () => {
242-
this.setState({ error: null, loading: true, timedOut: false })
243-
res = loadFn(opts.loader)
244-
this._loadModule()
270+
getCurrentValue () {
271+
return {
272+
...this._state,
273+
error: this._res.error,
274+
loaded: this._res.loaded,
275+
loading: this._res.loading
245276
}
277+
}
246278

247-
render () {
248-
if (this.state.loading || this.state.error) {
249-
return React.createElement(opts.loading, {
250-
isLoading: this.state.loading,
251-
pastDelay: this.state.pastDelay,
252-
timedOut: this.state.timedOut,
253-
error: this.state.error,
254-
retry: this.retry
255-
})
256-
} else if (this.state.loaded) {
257-
return opts.render(this.state.loaded, this.props)
258-
} else {
259-
return null
260-
}
279+
subscribe (callback) {
280+
this._callbacks.add(callback)
281+
return () => {
282+
this._callbacks.delete(callback)
261283
}
262284
}
263285
}

packages/next/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
"terser": "4.0.0",
122122
"unfetch": "4.1.0",
123123
"url": "0.11.0",
124+
"use-subscription": "1.1.1",
124125
"watchpack": "2.0.0-beta.5",
125126
"webpack": "4.39.0",
126127
"webpack-dev-middleware": "3.7.0",

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14577,6 +14577,11 @@ url@0.11.0, url@^0.11.0:
1457714577
punycode "1.3.2"
1457814578
querystring "0.2.0"
1457914579

14580+
use-subscription@1.1.1:
14581+
version "1.1.1"
14582+
resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.1.1.tgz#5509363e9bb152c4fb334151d4dceb943beaa7bb"
14583+
integrity sha512-gk4fPTYvNhs6Ia7u8/+K7bM7sZ7O7AMfWtS+zPO8luH+zWuiGgGcrW0hL4MRWZSzXo+4ofNorf87wZwBKz2YdQ==
14584+
1458014585
use@^3.1.0:
1458114586
version "3.1.1"
1458214587
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"

0 commit comments

Comments
 (0)