diff --git a/packages/next/shared/lib/dynamic.tsx b/packages/next/shared/lib/dynamic.tsx
index c46d12302cc0873..76ac1f12e4f586a 100644
--- a/packages/next/shared/lib/dynamic.tsx
+++ b/packages/next/shared/lib/dynamic.tsx
@@ -34,7 +34,7 @@ export type LoadableBaseOptions
= LoadableGeneratedOptions & {
| React.LazyExoticComponent>
loadableGenerated?: LoadableGeneratedOptions
ssr?: boolean
- unstable_suspense?: boolean
+ suspense?: boolean
}
export type LoadableOptions = LoadableBaseOptions
@@ -115,25 +115,25 @@ export default function dynamic
(
if (!process.env.__NEXT_REACT_ROOT) {
if (
process.env.NODE_ENV !== 'production' &&
- loadableOptions.unstable_suspense &&
+ loadableOptions.suspense &&
!isServerSide
) {
console.warn(
`Enable experimental.reactRoot or use React version above 18 to use suspense option`
)
}
- loadableOptions.unstable_suspense = false
+ loadableOptions.suspense = false
}
- const { unstable_suspense, loader } = loadableOptions
- // If unstable_suspense is enabled, delegate rendering to unstable_suspense
- if (unstable_suspense) {
+ const { suspense, loader } = loadableOptions
+ // If suspense is enabled, delegate rendering to suspense
+ if (suspense) {
delete loadableOptions.loadableGenerated
delete loadableOptions.loading
delete loadableOptions.ssr
}
- if (typeof loadableOptions.loader === 'function' && unstable_suspense) {
+ if (typeof loadableOptions.loader === 'function' && suspense) {
loadableOptions.loader = React.lazy(
loader as () => Promise<{
default: React.ComponentType
@@ -150,7 +150,7 @@ export default function dynamic
(
}
// support for disabling server side rendering, eg: dynamic(import('../hello-world'), {ssr: false})
- if (loadableOptions.ssr === false && !unstable_suspense) {
+ if (loadableOptions.ssr === false && !suspense) {
return noSSR(loadableFn, loadableOptions)
}
diff --git a/packages/next/shared/lib/loadable.js b/packages/next/shared/lib/loadable.js
index 103f04dc40c550d..b1382f80b3b6628 100644
--- a/packages/next/shared/lib/loadable.js
+++ b/packages/next/shared/lib/loadable.js
@@ -66,14 +66,11 @@ function createLoadableComponent(loadFn, options) {
timeout: null,
webpack: null,
modules: null,
- unstable_suspense: false,
+ suspense: false,
},
options
)
- opts.suspense = opts.unstable_suspense
- delete opts.unstable_suspense
-
let subscription = null
function init() {
if (opts.suspense) {
diff --git a/test/integration/react-18/prerelease/components/dynamic-suspense.js b/test/integration/react-18/prerelease/components/dynamic-hello.js
similarity index 64%
rename from test/integration/react-18/prerelease/components/dynamic-suspense.js
rename to test/integration/react-18/prerelease/components/dynamic-hello.js
index 3ec76eb42e6b9c7..c307ed31c33bbee 100644
--- a/test/integration/react-18/prerelease/components/dynamic-suspense.js
+++ b/test/integration/react-18/prerelease/components/dynamic-hello.js
@@ -1,12 +1,15 @@
import { Suspense } from 'react'
import dynamic from 'next/dynamic'
+const ssr = false
+const suspense = false
+
const Hello = dynamic(() => import('./hello'), {
- ssr: false,
- unstable_suspense: false,
+ ssr,
+ suspense: suspense,
})
-export default function SuspenseNoSSR({ thrown }) {
+export default function DynamicHello({ thrown }) {
return (
diff --git a/test/integration/react-18/prerelease/next.config.js b/test/integration/react-18/prerelease/next.config.js
index fa78a6452b85f9f..2142b50d92eef03 100644
--- a/test/integration/react-18/prerelease/next.config.js
+++ b/test/integration/react-18/prerelease/next.config.js
@@ -1,6 +1,7 @@
module.exports = {
experimental: {
reactRoot: true,
+ concurrentFeatures: false,
},
webpack(config) {
const { alias } = config.resolve
diff --git a/test/integration/react-18/prerelease/pages/suspense/no-thrown.js b/test/integration/react-18/prerelease/pages/suspense/no-thrown.js
index d2a0f043536f53b..a698358b8ffaa8a 100644
--- a/test/integration/react-18/prerelease/pages/suspense/no-thrown.js
+++ b/test/integration/react-18/prerelease/pages/suspense/no-thrown.js
@@ -1,5 +1,5 @@
-import DynamicSuspense from '../../components/dynamic-suspense'
+import Hello from '../../components/dynamic-hello'
export default function NoThrown() {
- return
+ return
}
diff --git a/test/integration/react-18/prerelease/pages/suspense/thrown.js b/test/integration/react-18/prerelease/pages/suspense/thrown.js
index 2b4ea326f3af7c3..fabf6fc174d9016 100644
--- a/test/integration/react-18/prerelease/pages/suspense/thrown.js
+++ b/test/integration/react-18/prerelease/pages/suspense/thrown.js
@@ -1,5 +1,5 @@
-import DynamicSuspense from '../../components/dynamic-suspense'
+import DynamicHello from '../../components/dynamic-hello'
export default function Thrown() {
- return
+ return
}
diff --git a/test/integration/react-18/test/concurrent.js b/test/integration/react-18/test/concurrent.js
new file mode 100644
index 000000000000000..08531bdea19539d
--- /dev/null
+++ b/test/integration/react-18/test/concurrent.js
@@ -0,0 +1,56 @@
+import { join } from 'path'
+import webdriver from 'next-webdriver'
+import cheerio from 'cheerio'
+import { File, check } from 'next-test-utils'
+
+export default (context, render) => {
+ const dynamicHello = new File(
+ join(context.appDir, 'components/dynamic-hello.js')
+ )
+ const nextConfig = new File(join(context.appDir, 'next.config.js'))
+
+ async function get$(path, query) {
+ const html = await render(path, query)
+ return cheerio.load(html)
+ }
+
+ describe('concurrentFeatures is enabled', () => {
+ beforeAll(() => {
+ dynamicHello.replace('const suspense = false', `const suspense = true`)
+ nextConfig.replace(
+ 'concurrentFeatures: false',
+ 'concurrentFeatures: true'
+ )
+ })
+ afterAll(() => {
+ dynamicHello.restore()
+ nextConfig.restore()
+ })
+
+ it('should render the fallback on server side if not suspended on server', async () => {
+ const $ = await get$('/suspense/no-thrown')
+ const html = $('body').html()
+ expect(html).toContain('loading')
+ expect(JSON.parse($('#__NEXT_DATA__').text()).dynamicIds).toBeUndefined()
+ })
+
+ it('should render the fallback on server side if suspended on server', async () => {
+ const $ = await get$('/suspense/thrown')
+ const html = $('body').html()
+ expect(html).toContain('loading')
+ expect(JSON.parse($('#__NEXT_DATA__').text()).dynamicIds).toBeUndefined()
+ })
+
+ it('should hydrate suspenses on client side if suspended on server', async () => {
+ let browser
+ try {
+ browser = await webdriver(context.appPort, '/suspense/thrown')
+ await check(() => browser.elementByCss('body').text(), /hello/)
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
+ })
+}
diff --git a/test/integration/react-18/test/dynamic.js b/test/integration/react-18/test/dynamic.js
index 5f1be25b0f7c47b..bd57cac1a3ae415 100644
--- a/test/integration/react-18/test/dynamic.js
+++ b/test/integration/react-18/test/dynamic.js
@@ -1,41 +1,29 @@
-/* eslint-env jest */
import { join } from 'path'
import webdriver from 'next-webdriver'
import cheerio from 'cheerio'
-import { check } from 'next-test-utils'
-import { File } from 'next-test-utils'
+import { File, check } from 'next-test-utils'
-const appDir = join(__dirname, '../prerelease')
-const page = new File(join(appDir, 'components/dynamic-suspense.js'))
-
-function writeDynamicTestComponent({ ssr, suspense = false }) {
- const content = `import { Suspense } from 'react'
- import dynamic from 'next/dynamic'
-
- const Hello = dynamic(() => import('./hello'), {
- ${typeof ssr !== 'undefined' ? `ssr: ${ssr},` : ''}
- unstable_suspense: ${suspense},
- })
+export default (context, render) => {
+ const dynamicHello = new File(
+ join(context.appDir, 'components/dynamic-hello.js')
+ )
- export default function SuspenseNoSSR({ thrown }) {
- return (
-
-
-
+ function updateComponent({ ssr, suspense = false }) {
+ dynamicHello.replace('const ssr = false', `const ssr = ${ssr + ''}`)
+ dynamicHello.replace(
+ 'const suspense = false',
+ `const suspense = ${suspense + ''}`
)
- }`
- page.write(content)
-}
+ }
-export default (context, render) => {
async function get$(path, query) {
const html = await render(path, query)
return cheerio.load(html)
}
describe('suspense:true option', () => {
- beforeAll(() => writeDynamicTestComponent({ suspense: true }))
- afterAll(() => page.restore())
+ beforeAll(() => updateComponent({ suspense: true }))
+ afterAll(() => dynamicHello.restore())
describe('promise is thrown on server side', () => {
// let `ssr` option be auto overridden
diff --git a/test/integration/react-18/test/index.test.js b/test/integration/react-18/test/index.test.js
index f09ed078047794f..a2d6dccaf16a0bd 100644
--- a/test/integration/react-18/test/index.test.js
+++ b/test/integration/react-18/test/index.test.js
@@ -15,6 +15,7 @@ import {
renderViaHTTP,
} from 'next-test-utils'
import dynamic from './dynamic'
+import concurrent from './concurrent'
jest.setTimeout(1000 * 60 * 5)
@@ -145,13 +146,30 @@ describe('React 18 basics', () => {
})
describe('Dynamic import', () => {
- const context = {}
+ const context = { appDir }
beforeEach(async () => {
context.appPort = await findPort()
- context.server = await launchApp(appDir, context.appPort, { nodeArgs })
+ context.server = await launchApp(context.appDir, context.appPort, {
+ nodeArgs,
+ })
})
afterEach(async () => {
await killApp(context.server)
})
dynamic(context, (p, q) => renderViaHTTP(context.appPort, p, q))
})
+
+describe('Concurrent mode', () => {
+ const context = { appDir }
+ beforeEach(async () => {
+ await nextBuild(context.appDir, [], { nodeArgs })
+ context.appPort = await findPort()
+ context.server = await nextStart(context.appDir, context.appPort, {
+ nodeArgs,
+ })
+ })
+ afterEach(async () => {
+ await killApp(context.server)
+ })
+ concurrent(context, (p, q) => renderViaHTTP(context.appPort, p, q))
+})