Currently, `next/dynamic` will opt-in to `React.lazy` if `{ suspense: true }` is used. And React 18 will always resolve the `Suspense` boundary on the server-side, effectively ignoring the `ssr` option. The PR fixes #39609 by showing a warning message when `{ suspense: true, ssr: false }` is detected. The error documentation and the corresponding test case has also been updated. In the future, Next.js could implement a custom version of `React.lazy` that could suspense without executing the lazy-loaded component on the server-side. cc @huozhi ## Bug - [x] Related issues linked using `fixes #number` - [x] Integration tests added - [x] Errors have helpful link attached, see `contributing.md`
This commit is contained in:
parent
78aefee1c6
commit
5360440413
6 changed files with 108 additions and 57 deletions
|
@ -2,11 +2,29 @@
|
|||
|
||||
#### Why This Error Occurred
|
||||
|
||||
`<Suspense>` is not allowed under legacy render mode when using React older than v18.
|
||||
- You are using `{ suspense: true }` with React version older than 18.
|
||||
- You are using `{ suspense: true, ssr: false }`.
|
||||
- You are using `{ suspense: true, loading }`.
|
||||
|
||||
#### Possible Ways to Fix It
|
||||
|
||||
Remove `suspense: true` option in `next/dynamic` usages.
|
||||
**If you are using `{ suspense: true }` with React version older than 18**
|
||||
|
||||
- You can try upgrading to React 18 or newer
|
||||
- If upgrading React is not an option, remove `{ suspense: true }` from `next/dynamic` usages.
|
||||
|
||||
**If you are using `{ suspense: true, ssr: false }`**
|
||||
|
||||
Next.js will use `React.lazy` when `suspense` is set to true. React 18 or newer will always try to resolve the Suspense boundary on the server. This behavior can not be disabled, thus the `ssr: false` is ignored with `suspense: true`.
|
||||
|
||||
- You should write code that works in both client-side and server-side.
|
||||
- If rewriting the code is not an option, remove `{ suspense: true }` from `next/dynamic` usages.
|
||||
|
||||
**If you are using `{ suspense: true, loading }`**
|
||||
|
||||
Next.js will use `React.lazy` when `suspense` is set to true, when your dynamic-imported component is loading, React will use the closest suspense boundary's fallback.
|
||||
|
||||
You should remove `loading` from `next/dynamic` usages, and use `<Suspense />`'s `fallback` prop.
|
||||
|
||||
### Useful Links
|
||||
|
||||
|
|
|
@ -112,6 +112,31 @@ export default function dynamic<P = {}>(
|
|||
)
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (loadableOptions.suspense) {
|
||||
/**
|
||||
* TODO: Currently, next/dynamic will opt-in to React.lazy if { suspense: true } is used
|
||||
* React 18 will always resolve the Suspense boundary on the server-side, effectively ignoring the ssr option
|
||||
*
|
||||
* In the future, when React Suspense with third-party libraries is stable, we can implement a custom version of
|
||||
* React.lazy that can suspense on the server-side while only loading the component on the client-side
|
||||
*/
|
||||
if (loadableOptions.ssr === false) {
|
||||
loadableOptions.ssr = true
|
||||
console.warn(
|
||||
`"ssr: false" is ignored by next/dynamic because you can not enable "suspense" while disabling "ssr" at the same time. Read more: https://nextjs.org/docs/messages/invalid-dynamic-suspense`
|
||||
)
|
||||
}
|
||||
|
||||
if (loadableOptions.loading != null) {
|
||||
loadableOptions.loading = undefined
|
||||
console.warn(
|
||||
`"loading" is ignored by next/dynamic because you have enabled "suspense". Place your loading element in your suspense boundary's "fallback" prop instead. Read more: https://nextjs.org/docs/messages/invalid-dynamic-suspense`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// coming from build/babel/plugins/react-loadable-plugin.js
|
||||
if (loadableOptions.loadableGenerated) {
|
||||
loadableOptions = {
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
import { createNext } from 'e2e-utils'
|
||||
import { NextInstance } from 'test/lib/next-modes/base'
|
||||
import { hasRedbox, renderViaHTTP } from 'next-test-utils'
|
||||
import webdriver from 'next-webdriver'
|
||||
|
||||
const suite =
|
||||
process.env.NEXT_TEST_REACT_VERSION === '^17' ? describe.skip : describe
|
||||
|
||||
// Skip the suspense test if react version is 17
|
||||
suite('dynamic with suspense', () => {
|
||||
let next: NextInstance
|
||||
|
||||
beforeAll(async () => {
|
||||
next = await createNext({
|
||||
files: {
|
||||
'pages/index.js': `
|
||||
import { Suspense } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const Thing = dynamic(() => import("./thing"), { ssr: false, suspense: true });
|
||||
|
||||
export default function IndexPage() {
|
||||
return (
|
||||
<div>
|
||||
<p>Next.js Example</p>
|
||||
<Suspense fallback="Loading...">
|
||||
<Thing />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
`,
|
||||
'pages/thing.js': `
|
||||
export default function Thing() {
|
||||
return "Thing";
|
||||
}
|
||||
`,
|
||||
},
|
||||
dependencies: {},
|
||||
})
|
||||
})
|
||||
afterAll(() => next.destroy())
|
||||
|
||||
it('should render server-side', async () => {
|
||||
const html = await renderViaHTTP(next.url, '/')
|
||||
expect(html).toContain('Next.js Example')
|
||||
expect(html).toContain('Thing')
|
||||
})
|
||||
|
||||
it('should render client-side', async () => {
|
||||
const browser = await webdriver(next.url, '/')
|
||||
expect(await hasRedbox(browser)).toBe(false)
|
||||
await browser.close()
|
||||
})
|
||||
})
|
19
test/integration/next-dynamic-with-suspense/pages/index.js
Normal file
19
test/integration/next-dynamic-with-suspense/pages/index.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Suspense } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const Thing = dynamic(() => import('./thing'), {
|
||||
ssr: false,
|
||||
suspense: true,
|
||||
loading: () => 'Loading...',
|
||||
})
|
||||
|
||||
export default function IndexPage() {
|
||||
return (
|
||||
<div>
|
||||
<p>Next.js Example</p>
|
||||
<Suspense fallback="Loading...">
|
||||
<Thing />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export default function Thing() {
|
||||
return 'Thing'
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/* eslint-env jest */
|
||||
|
||||
import webdriver from 'next-webdriver'
|
||||
import { join } from 'path'
|
||||
import {
|
||||
renderViaHTTP,
|
||||
findPort,
|
||||
launchApp,
|
||||
killApp,
|
||||
hasRedbox,
|
||||
} from 'next-test-utils'
|
||||
|
||||
let app
|
||||
let appPort: number
|
||||
const appDir = join(__dirname, '../')
|
||||
|
||||
describe('next/dynamic with suspense', () => {
|
||||
beforeAll(async () => {
|
||||
appPort = await findPort()
|
||||
app = await launchApp(appDir, appPort)
|
||||
})
|
||||
afterAll(() => killApp(app))
|
||||
|
||||
it('should render server-side', async () => {
|
||||
const html = await renderViaHTTP(appPort, '/')
|
||||
expect(html).toContain('Next.js Example')
|
||||
expect(html).toContain('Thing')
|
||||
})
|
||||
|
||||
it('should render client-side', async () => {
|
||||
const browser = await webdriver(appPort, '/')
|
||||
const warnings = (await browser.log()).map((log) => log.message).join('\n')
|
||||
|
||||
expect(await hasRedbox(browser)).toBe(false)
|
||||
expect(warnings).toMatch(
|
||||
/"ssr: false" is ignored by next\/dynamic because you can not enable "suspense" while disabling "ssr" at the same time/gim
|
||||
)
|
||||
|
||||
await browser.close()
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue