Skip to content

Commit

Permalink
Use react.lazy in dynamic when suspense = true
Browse files Browse the repository at this point in the history
  • Loading branch information
huozhi committed Aug 13, 2021
1 parent 6014b6e commit 4a994fc
Show file tree
Hide file tree
Showing 29 changed files with 445 additions and 83 deletions.
9 changes: 6 additions & 3 deletions packages/next/build/webpack-config.ts
Expand Up @@ -769,9 +769,9 @@ export default async function getBaseWebpackConfig(

if (isLocal) {
// Makes sure dist/shared and dist/server are not bundled
// we need to process shared/lib/router/router so that
// the DefinePlugin can inject process.env values
const isNextExternal = /next[/\\]dist[/\\](shared|server)[/\\](?!lib[/\\]router[/\\]router)/.test(
// we need to process shared `router/router` and `dynamic`,
// so that the DefinePlugin can inject process.env values
const isNextExternal = /next[/\\]dist[/\\](shared|server)[/\\](?!lib[/\\](router[/\\]router|dynamic))/.test(
res
)

Expand Down Expand Up @@ -1185,6 +1185,9 @@ export default async function getBaseWebpackConfig(
config.reactStrictMode
),
'process.env.__NEXT_REACT_ROOT': JSON.stringify(hasReactRoot),
'process.env.__NEXT_CONCURRENT_FEATURES': JSON.stringify(
config.experimental.concurrentFeatures && hasReactRoot
),
'process.env.__NEXT_OPTIMIZE_FONTS': JSON.stringify(
config.optimizeFonts && !dev
),
Expand Down
1 change: 1 addition & 0 deletions packages/next/export/index.ts
Expand Up @@ -378,6 +378,7 @@ export default async function exportApp(
disableOptimizedLoading: nextConfig.experimental.disableOptimizedLoading,
// TODO: We should support dynamic HTML too
requireStaticHTML: true,
concurrentFeatures: nextConfig.experimental.concurrentFeatures,
}

const { serverRuntimeConfig, publicRuntimeConfig } = nextConfig
Expand Down
26 changes: 22 additions & 4 deletions packages/next/shared/lib/dynamic.tsx
Expand Up @@ -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
Expand All @@ -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>
Expand Down Expand Up @@ -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
}
if (suspenseOptions.suspense) {
return loadableFn(suspenseOptions)
}

// coming from build/babel/plugins/react-loadable-plugin.js
if (loadableOptions.loadableGenerated) {
loadableOptions = {
Expand Down
21 changes: 15 additions & 6 deletions packages/next/shared/lib/loadable.js
Expand Up @@ -66,12 +66,16 @@ function createLoadableComponent(loadFn, options) {
timeout: null,
webpack: null,
modules: null,
suspense: false,
},
options
)

let subscription = null
if (opts.suspense) {
opts.lazy = React.lazy(opts.loader)
}

let subscription = null
function init() {
if (!subscription) {
const sub = new LoadableSubscription(loadFn, opts)
Expand All @@ -86,7 +90,7 @@ function createLoadableComponent(loadFn, options) {
}

// Server only
if (typeof window === 'undefined') {
if (typeof window === 'undefined' && !opts.suspense) {
ALL_INITIALIZERS.push(init)
}

Expand All @@ -95,7 +99,8 @@ function createLoadableComponent(loadFn, options) {
!initialized &&
typeof window !== 'undefined' &&
typeof opts.webpack === 'function' &&
typeof require.resolveWeak === 'function'
typeof require.resolveWeak === 'function' &&
!opts.suspense
) {
const moduleIds = opts.webpack()
READY_INITIALIZERS.push((ids) => {
Expand All @@ -107,12 +112,11 @@ function createLoadableComponent(loadFn, options) {
})
}

const LoadableComponent = (props, ref) => {
function LoadableImpl(props, ref) {
init()

const context = React.useContext(LoadableContext)
const state = useSubscription(subscription)

React.useImperativeHandle(
ref,
() => ({
Expand Down Expand Up @@ -144,7 +148,12 @@ function createLoadableComponent(loadFn, options) {
}, [props, state])
}

LoadableComponent.preload = () => init()
function LazyImpl(props, ref) {
return React.createElement(opts.lazy, { ...props, ref })
}

const LoadableComponent = opts.suspense ? LazyImpl : LoadableImpl
LoadableComponent.preload = () => !opts.suspense && init()
LoadableComponent.displayName = 'LoadableComponent'

return React.forwardRef(LoadableComponent)
Expand Down
1 change: 0 additions & 1 deletion test/integration/chunking/test/index.test.js
Expand Up @@ -84,7 +84,6 @@ describe('Chunking', () => {

it('should execute the build manifest', async () => {
const html = await renderViaHTTP(appPort, '/')
console.log(html)
const $ = cheerio.load(html)
expect(
Array.from($('script'))
Expand Down
24 changes: 24 additions & 0 deletions test/integration/react-18/app/components/bar.js
@@ -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>
)
}
18 changes: 18 additions & 0 deletions test/integration/react-18/app/components/dynamic-hello.js
@@ -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>
)
}
3 changes: 3 additions & 0 deletions test/integration/react-18/app/components/foo.js
@@ -0,0 +1,3 @@
export default function Foo() {
return 'foo'
}
13 changes: 13 additions & 0 deletions test/integration/react-18/app/components/hello.js
@@ -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>
}
37 changes: 37 additions & 0 deletions test/integration/react-18/app/components/promise-cache.js
@@ -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
}
@@ -1,4 +1,8 @@
module.exports = {
experimental: {
reactRoot: true,
// concurrentFeatures: true,
},
webpack(config) {
const { alias } = config.resolve
// FIXME: resolving react/jsx-runtime https://github.com/facebook/react/issues/20235
Expand Down
12 changes: 12 additions & 0 deletions test/integration/react-18/app/package.json
@@ -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": "*"
}
}
13 changes: 13 additions & 0 deletions test/integration/react-18/app/pages/_app.js
@@ -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
12 changes: 12 additions & 0 deletions test/integration/react-18/app/pages/index.js
@@ -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>
)
}
21 changes: 21 additions & 0 deletions test/integration/react-18/app/pages/suspense/no-preload.js
@@ -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')],
},
})

export default function NoPreload() {
return (
<Suspense fallback={'rab'}>
<Bar />
</Suspense>
)
}
5 changes: 5 additions & 0 deletions test/integration/react-18/app/pages/suspense/no-thrown.js
@@ -0,0 +1,5 @@
import DynamicHello from '../../components/dynamic-hello'

export default function NoThrown() {
return <DynamicHello name="no-thrown" thrown={false} />
}
5 changes: 5 additions & 0 deletions test/integration/react-18/app/pages/suspense/thrown.js
@@ -0,0 +1,5 @@
import DynamicHello from '../../components/dynamic-hello'

export default function Thrown() {
return <DynamicHello name="thrown" thrown />
}
10 changes: 10 additions & 0 deletions test/integration/react-18/app/pages/suspense/unwrapped.js
@@ -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 />
}
1 change: 0 additions & 1 deletion test/integration/react-18/prerelease/.gitignore

This file was deleted.

This file was deleted.

6 changes: 0 additions & 6 deletions test/integration/react-18/prerelease/package.json

This file was deleted.

3 changes: 0 additions & 3 deletions test/integration/react-18/prerelease/pages/bar.js

This file was deleted.

16 changes: 0 additions & 16 deletions test/integration/react-18/prerelease/pages/index.js

This file was deleted.

3 changes: 0 additions & 3 deletions test/integration/react-18/supported/pages/index.js

This file was deleted.

0 comments on commit 4a994fc

Please sign in to comment.