diff --git a/packages/next/shared/lib/dynamic.tsx b/packages/next/shared/lib/dynamic.tsx index ba5871238998..967901839267 100644 --- a/packages/next/shared/lib/dynamic.tsx +++ b/packages/next/shared/lib/dynamic.tsx @@ -114,15 +114,11 @@ export default function dynamic

( loadableOptions = { ...loadableOptions, ...options } const suspenseOptions = loadableOptions as LoadableSuspenseOptions

- if (!process.env.__NEXT_CONCURRENT_FEATURES) { - // Error if react root is not enabled and `suspense` option is set to true - if (!process.env.__NEXT_REACT_ROOT && suspenseOptions.suspense) { - // TODO: add error doc when this feature is stable - throw new Error( - `Disallowed suspense option usage with next/dynamic in blocking mode` - ) - } - suspenseOptions.suspense = false + if (!process.env.__NEXT_CONCURRENT_FEATURES && suspenseOptions.suspense) { + // TODO: add error doc when this feature is stable + throw new Error( + `Disallowed suspense option usage with next/dynamic in blocking mode` + ) } if (suspenseOptions.suspense) { return loadableFn(suspenseOptions) diff --git a/packages/next/shared/lib/loadable.js b/packages/next/shared/lib/loadable.js index fbeaf9480f6f..6e3fee37504c 100644 --- a/packages/next/shared/lib/loadable.js +++ b/packages/next/shared/lib/loadable.js @@ -71,8 +71,14 @@ function createLoadableComponent(loadFn, options) { options ) + let lazyComponent if (opts.suspense) { - opts.lazy = React.lazy(opts.loader) + lazyComponent = React.lazy(opts.loader) + return LazyImpl + } + + function LazyImpl(props) { + return React.createElement(lazyComponent, props) } let subscription = null @@ -90,7 +96,7 @@ function createLoadableComponent(loadFn, options) { } // Server only - if (typeof window === 'undefined' && !opts.suspense) { + if (typeof window === 'undefined') { ALL_INITIALIZERS.push(init) } @@ -99,8 +105,7 @@ function createLoadableComponent(loadFn, options) { !initialized && typeof window !== 'undefined' && typeof opts.webpack === 'function' && - typeof require.resolveWeak === 'function' && - !opts.suspense + typeof require.resolveWeak === 'function' ) { const moduleIds = opts.webpack() READY_INITIALIZERS.push((ids) => { @@ -148,12 +153,8 @@ function createLoadableComponent(loadFn, options) { }, [props, state]) } - function LazyImpl(props, ref) { - return React.createElement(opts.lazy, { ...props, ref }) - } - - const LoadableComponent = opts.suspense ? LazyImpl : LoadableImpl - LoadableComponent.preload = () => !opts.suspense && init() + const LoadableComponent = LoadableImpl + LoadableComponent.preload = () => init() LoadableComponent.displayName = 'LoadableComponent' return React.forwardRef(LoadableComponent) diff --git a/test/integration/react-18/app/pages/suspense/unwrapped.js b/test/integration/react-18/app/pages/suspense/unwrapped.js deleted file mode 100644 index 3909e80679a6..000000000000 --- a/test/integration/react-18/app/pages/suspense/unwrapped.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' -import dynamic from 'next/dynamic' - -const Hello = dynamic(() => import('../../components/hello'), { - suspense: true, -}) - -export default function Unwrapped() { - return -} diff --git a/test/integration/react-18/test/basics.js b/test/integration/react-18/test/basics.js deleted file mode 100644 index bb3c4ad99ced..000000000000 --- a/test/integration/react-18/test/basics.js +++ /dev/null @@ -1,31 +0,0 @@ -/* eslint-env jest */ - -import webdriver from 'next-webdriver' -import cheerio from 'cheerio' -import { fetchViaHTTP, renderViaHTTP } from 'next-test-utils' - -export default (context) => { - it('hydrates correctly for normal page', async () => { - const browser = await webdriver(context.appPort, '/') - expect(await browser.eval('window.didHydrate')).toBe(true) - expect(await browser.elementById('react-dom-version').text()).toMatch(/18/) - }) - - it('should works with suspense in ssg', async () => { - const res1 = await fetchViaHTTP(context.appPort, '/suspense/thrown') - const res2 = await fetchViaHTTP(context.appPort, '/suspense/no-thrown') - - expect(res1.status).toBe(200) - expect(res2.status).toBe(200) - }) - - it('should render fallback without preloads on server side', async () => { - const html = await renderViaHTTP(context.appPort, '/suspense/no-preload') - const $ = cheerio.load(html) - const nextData = JSON.parse($('#__NEXT_DATA__').text()) - const content = $('#__next').text() - // is suspended - expect(content).toBe('rab') - expect(nextData.dynamicIds).toBeUndefined() - }) -} diff --git a/test/integration/react-18/test/blocking.js b/test/integration/react-18/test/blocking.js deleted file mode 100644 index 3adb476c5a01..000000000000 --- a/test/integration/react-18/test/blocking.js +++ /dev/null @@ -1,27 +0,0 @@ -/* eslint-env jest */ - -import cheerio from 'cheerio' - -export default (context, render) => { - async function get$(path, query) { - const html = await render(path, query) - return cheerio.load(html) - } - - it('should render fallback on server side if suspense without preload', async () => { - const $ = await get$('/suspense/no-preload') - const nextData = JSON.parse($('#__NEXT_DATA__').text()) - const content = $('#__next').text() - expect(content).toBe('rab') - expect(nextData.dynamicIds).toBeUndefined() - }) - - it('should render fallback on server side if suspended on server with preload', async () => { - const $ = await get$('/suspense/thrown') - const html = $('body').html() - expect(html).toContain('loading') - expect( - JSON.parse($('#__NEXT_DATA__').text()).dynamicIds - ).not.toBeUndefined() - }) -} diff --git a/test/integration/react-18/test/index.test.js b/test/integration/react-18/test/index.test.js index d0f889adcccd..e4cd2225bf52 100644 --- a/test/integration/react-18/test/index.test.js +++ b/test/integration/react-18/test/index.test.js @@ -12,9 +12,7 @@ import { nextStart, renderViaHTTP, } from 'next-test-utils' -import blocking from './blocking' import concurrent from './concurrent' -import basics from './basics' jest.setTimeout(1000 * 60 * 5) @@ -80,14 +78,6 @@ describe('React 18 Support', () => { expect(output).not.toMatch(USING_CREATE_ROOT) expect(output).not.toMatch(UNSUPPORTED_PRERELEASE) }) - - it('suspense is not allowed in blocking rendering mode', async () => { - const appPort = await findPort() - const app = await launchApp(appDir, appPort) - const html = await renderViaHTTP(appPort, '/suspense/unwrapped') - await killApp(app) - expect(html).toContain(SUSPENSE_ERROR_MESSAGE) - }) }) describe('warns with stable supported version of react-dom', () => { @@ -117,13 +107,6 @@ describe('React 18 Support', () => { }) }) -describe('Basics', () => { - runTests('default setting with react 18', 'dev', (context) => basics(context)) - runTests('default setting with react 18', 'prod', (context) => - basics(context) - ) -}) - describe('Blocking mode', () => { beforeAll(() => { dynamicHello.replace('suspense = false', `suspense = true`) @@ -132,13 +115,10 @@ describe('Blocking mode', () => { dynamicHello.restore() }) - runTests('concurrentFeatures is disabled', 'dev', (context) => - blocking(context, (p, q) => renderViaHTTP(context.appPort, p, q)) - ) - - runTests('concurrentFeatures is disabled', 'prod', (context) => - blocking(context, (p, q) => renderViaHTTP(context.appPort, p, q)) - ) + it('suspense is not allowed in blocking rendering mode (prod)', async () => { + const output = await getBuildOutput(appDir) + expect(output).toContain(SUSPENSE_ERROR_MESSAGE) + }) }) describe('Concurrent mode', () => {