1
0
Fork 0
mirror of https://github.com/terribleplan/next.js.git synced 2024-01-19 02:48:18 +00:00

Fix recursive hydration of next/dynamic (#6326)

Fixes #5347

The main issue is that we were waiting only 1 level of dynamic imports, so the dynamic imports nested inside other dynamic import files were not awaited. This would cause either a flash of loading states or you wouldn't see the loading state (because of preload) but it would then show a hydration warning in development.

Thanks to @arthens for providing the reproduction that I modelled the tests after.
This commit is contained in:
Tim Neutkens 2019-02-17 19:52:00 +01:00 committed by GitHub
parent 5cef35b811
commit dd9811b206
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 63 additions and 26 deletions

View file

@ -25,7 +25,7 @@ import React from 'react'
import PropTypes from 'prop-types'
const ALL_INITIALIZERS = []
const READY_INITIALIZERS = new Map()
const READY_INITIALIZERS = []
let initialized = false
function load (loader) {
@ -138,11 +138,13 @@ function createLoadableComponent (loadFn, options) {
// Client only
if (!initialized && typeof window !== 'undefined' && typeof opts.webpack === 'function') {
const moduleIds = opts.webpack()
for (const moduleId of moduleIds) {
READY_INITIALIZERS.set(moduleId, () => {
return init()
})
}
READY_INITIALIZERS.push((ids) => {
for (const moduleId of moduleIds) {
if (ids.indexOf(moduleId) !== -1) {
return init()
}
}
})
}
return class LoadableComponent extends React.Component {
@ -273,17 +275,17 @@ function LoadableMap (opts) {
Loadable.Map = LoadableMap
function flushInitializers (initializers) {
function flushInitializers (initializers, ids) {
let promises = []
while (initializers.length) {
let init = initializers.pop()
promises.push(init())
promises.push(init(ids))
}
return Promise.all(promises).then(() => {
if (initializers.length) {
return flushInitializers(initializers)
return flushInitializers(initializers, ids)
}
})
}
@ -294,24 +296,14 @@ Loadable.preloadAll = () => {
})
}
Loadable.preloadReady = (webpackIds) => {
return new Promise((resolve, reject) => {
const initializers = webpackIds.reduce((allInitalizers, moduleId) => {
const initializer = READY_INITIALIZERS.get(moduleId)
if (!initializer) {
return allInitalizers
}
allInitalizers.push(initializer)
return allInitalizers
}, [])
initialized = true
// Make sure the object is cleared
READY_INITIALIZERS.clear()
Loadable.preloadReady = (ids) => {
return new Promise((resolve) => {
const res = () => {
initialized = true
return resolve()
}
// We always will resolve, errors should be handled within loading UIs.
flushInitializers(initializers).then(resolve, resolve)
flushInitializers(READY_INITIALIZERS, ids).then(res, res)
})
}

View file

@ -0,0 +1,8 @@
import dynamic from 'next/dynamic'
const Nested2 = dynamic(() => import('./nested2'))
export default () => <div>
Nested 1
<Nested2 />
</div>

View file

@ -0,0 +1,12 @@
import dynamic from 'next/dynamic'
const BrowserLoaded = dynamic(async () => () => <div>Browser hydrated</div>, {
ssr: false
})
export default () => <div>
<div>
Nested 2
</div>
<BrowserLoaded />
</div>

View file

@ -0,0 +1,5 @@
import dynamic from 'next/dynamic'
const DynamicComponent = dynamic(() => import('../../components/nested1'))
export default DynamicComponent

View file

@ -37,6 +37,26 @@ export default (context, render) => {
}
})
it('should hydrate nested chunks', async () => {
let browser
try {
browser = await webdriver(context.appPort, '/dynamic/nested')
await check(() => browser.elementByCss('body').text(), /Nested 1/)
await check(() => browser.elementByCss('body').text(), /Nested 2/)
await check(() => browser.elementByCss('body').text(), /Browser hydrated/)
const logs = await browser.log('browser')
logs.forEach(logItem => {
expect(logItem.message).not.toMatch(/Expected server HTML to contain/)
})
} finally {
if (browser) {
browser.close()
}
}
})
it('should render the component Head content', async () => {
let browser
try {