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)) +})