-
Notifications
You must be signed in to change notification settings - Fork 29.9k
Support suspense in next dynamic #27611
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,19 +33,24 @@ export type LoadableBaseOptions<P = {}> = LoadableGeneratedOptions & { | |
| ssr?: boolean | ||
| } | ||
|
|
||
| export type LoadableSuspenseOptions<P = {}> = { | ||
| loader: Loader<P> | ||
| suspense?: boolean | ||
| } | ||
|
|
||
| export type LoadableOptions<P = {}> = LoadableBaseOptions<P> | ||
|
|
||
| export type DynamicOptions<P = {}> = LoadableBaseOptions<P> | ||
|
|
||
| export type LoadableFn<P = {}> = ( | ||
| opts: LoadableOptions<P> | ||
| opts: LoadableOptions<P> | LoadableSuspenseOptions<P> | ||
| ) => React.ComponentType<P> | ||
|
|
||
| export type LoadableComponent<P = {}> = React.ComponentType<P> | ||
|
|
||
| export function noSSR<P = {}>( | ||
| LoadableInitializer: LoadableFn<P>, | ||
| loadableOptions: LoadableOptions<P> | ||
| loadableOptions: LoadableBaseOptions<P> | ||
| ): React.ComponentType<P> { | ||
| // Removing webpack and modules means react-loadable won't try preloading | ||
| delete loadableOptions.webpack | ||
|
|
@@ -63,8 +68,6 @@ export function noSSR<P = {}>( | |
| ) | ||
| } | ||
|
|
||
| // function dynamic<P = {}, O extends DynamicOptions>(options: O): | ||
|
|
||
| export default function dynamic<P = {}>( | ||
| dynamicOptions: DynamicOptions<P> | Loader<P>, | ||
| options?: DynamicOptions<P> | ||
|
|
@@ -110,6 +113,21 @@ export default function dynamic<P = {}>( | |
| // Support for passing options, eg: dynamic(import('../hello-world'), {loading: () => <p>Loading something</p>}) | ||
| loadableOptions = { ...loadableOptions, ...options } | ||
|
|
||
| const suspenseOptions = loadableOptions as LoadableSuspenseOptions<P> | ||
| 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to also show errors if users enable suspense w/o concurrent features?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we do, yes. Since it's also experimental.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suspense will be resolved as fallback on server side in blocking rendering, do we want to skip this scenario? I think this can expand use cases, throwing error will limit usage of client suspenses. |
||
| } | ||
| if (suspenseOptions.suspense) { | ||
| return loadableFn(suspenseOptions) | ||
| } | ||
|
|
||
| // coming from build/babel/plugins/react-loadable-plugin.js | ||
| if (loadableOptions.loadableGenerated) { | ||
| loadableOptions = { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { Suspense } from 'react' | ||
| import dynamic from 'next/dynamic' | ||
| import { useCachedPromise } from './promise-cache' | ||
|
|
||
| const Foo = dynamic(() => import('./foo'), { | ||
| suspense: true, | ||
| }) | ||
|
|
||
| export default function Bar() { | ||
| useCachedPromise( | ||
| 'bar', | ||
| () => new Promise((resolve) => setTimeout(resolve, 300)), | ||
| true | ||
| ) | ||
|
|
||
| return ( | ||
| <div> | ||
| bar | ||
| <Suspense fallback={'oof'}> | ||
| <Foo /> | ||
| </Suspense> | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { Suspense } from 'react' | ||
| import dynamic from 'next/dynamic' | ||
|
|
||
| let ssr | ||
| const suspense = false | ||
|
|
||
| const Hello = dynamic(() => import('./hello'), { | ||
| ssr, | ||
| suspense, | ||
| }) | ||
|
|
||
| export default function DynamicHello(props) { | ||
| return ( | ||
| <Suspense fallback={'loading'}> | ||
| <Hello {...props} /> | ||
| </Suspense> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export default function Foo() { | ||
| return 'foo' | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import React from 'react' | ||
| import ReactDOM from 'react-dom' | ||
| import { useCachedPromise } from './promise-cache' | ||
|
|
||
| export default function Hello({ name, thrown = false }) { | ||
| useCachedPromise( | ||
| name, | ||
| () => new Promise((resolve) => setTimeout(resolve, 200)), | ||
| thrown | ||
| ) | ||
|
|
||
| return <p>hello {ReactDOM.version}</p> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import React from 'react' | ||
|
|
||
| const PromiseCacheContext = React.createContext(null) | ||
|
|
||
| export const cache = new Map() | ||
| export const PromiseCacheProvider = PromiseCacheContext.Provider | ||
|
|
||
| export function useCachedPromise(key, fn, thrown = false) { | ||
| const cache = React.useContext(PromiseCacheContext) | ||
|
|
||
| if (!thrown) return undefined | ||
| let entry = cache.get(key) | ||
| if (!entry) { | ||
| entry = { | ||
| status: 'PENDING', | ||
| value: fn().then( | ||
| (value) => { | ||
| cache.set(key, { | ||
| status: 'RESOLVED', | ||
| value, | ||
| }) | ||
| }, | ||
| (err) => { | ||
| cache.set(key, { | ||
| status: 'REJECTED', | ||
| value: err, | ||
| }) | ||
| } | ||
| ), | ||
| } | ||
| cache.set(key, entry) | ||
| } | ||
| if (['PENDING', 'REJECTED'].includes(entry.status)) { | ||
| throw entry.value | ||
| } | ||
| return entry.value | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "scripts": { | ||
| "next": "node -r ../test/require-hook.js ../../../../packages/next/dist/bin/next", | ||
| "dev": "yarn next dev", | ||
| "build": "yarn next build", | ||
| "start": "yarn next start" | ||
| }, | ||
| "dependencies": { | ||
| "react": "*", | ||
| "react-dom": "*" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { PromiseCacheProvider } from '../components/promise-cache' | ||
|
|
||
| const cache = new Map() | ||
|
|
||
| function MyApp({ Component, pageProps }) { | ||
| return ( | ||
| <PromiseCacheProvider value={cache}> | ||
| <Component {...pageProps} /> | ||
| </PromiseCacheProvider> | ||
| ) | ||
| } | ||
|
|
||
| export default MyApp |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import ReactDOM from 'react-dom' | ||
|
|
||
| export default function Index() { | ||
| if (typeof window !== 'undefined') { | ||
| window.didHydrate = true | ||
| } | ||
| return ( | ||
| <div> | ||
| <p id="react-dom-version">{ReactDOM.version}</p> | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { Suspense } from 'react' | ||
| import dynamic from 'next/dynamic' | ||
|
|
||
| const Bar = dynamic(() => import('../../components/bar'), { | ||
| suspense: true, | ||
| // Explicitly declare loaded modules. | ||
| // For suspense cases, they'll be ignored. | ||
| // For loadable component cases, they'll be handled | ||
| loadableGenerated: { | ||
| modules: ['../../components/bar'], | ||
| webpack: [require.resolveWeak('../../components/bar')], | ||
| }, | ||
huozhi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }) | ||
|
|
||
| export default function NoPreload() { | ||
| return ( | ||
| <Suspense fallback={'rab'}> | ||
| <Bar /> | ||
| </Suspense> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import DynamicHello from '../../components/dynamic-hello' | ||
|
|
||
| export default function NoThrown() { | ||
| return <DynamicHello name="no-thrown" thrown={false} /> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import DynamicHello from '../../components/dynamic-hello' | ||
|
|
||
| export default function Thrown() { | ||
| return <DynamicHello name="thrown" thrown /> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import React from 'react' | ||
| import dynamic from 'next/dynamic' | ||
|
|
||
| const Hello = dynamic(() => import('../../components/hello'), { | ||
| suspense: true, | ||
| }) | ||
|
|
||
| export default function Unwrapped() { | ||
| return <Hello /> | ||
| } |
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.