Skip to content

Commit

Permalink
split lazy and loadable impl, update tests
Browse files Browse the repository at this point in the history
  • Loading branch information
huozhi committed Jul 30, 2021
1 parent f75ad9a commit d673bb5
Show file tree
Hide file tree
Showing 17 changed files with 165 additions and 73 deletions.
2 changes: 1 addition & 1 deletion docs/advanced-features/dynamic-import.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export default Home

## With suspense

In React 18, `<Suspense>` and `React.lazy` can just work with SSR. So dynamic provide an option `suspense` to let you choose if you want to load a component under suspense.
In React 18, `<Suspense>` and `React.lazy` can work with SSR. So dynamic provide an option `suspense` to let you choose if you want to load a component under suspense.

```jsx
import { Suspense } from 'react'
Expand Down
6 changes: 3 additions & 3 deletions packages/next/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,9 +755,9 @@ export default async function getBaseWebpackConfig(

if (isLocal) {
// Makes sure dist/shared and dist/server are not bundled
// we need to process `router/router`, `dynamic` and `loadable`
// under shared/lib so that the DefinePlugin can inject process.env values
const isNextExternal = /next[/\\]dist[/\\](shared|server)[/\\](?!lib[/\\](router[/\\]router|dynamic|loadable))/.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
5 changes: 1 addition & 4 deletions packages/next/server/render.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { IncomingMessage, ServerResponse } from 'http'
import { ParsedUrlQuery } from 'querystring'
import React from 'react'
import ReactDOM, {
renderToStaticMarkup,
renderToString,
} from 'react-dom/server'
import { renderToStaticMarkup, renderToString } from 'react-dom/server'
import { warn } from '../build/output/log'
import { UnwrapPromise } from '../lib/coalesced-function'
import {
Expand Down
37 changes: 19 additions & 18 deletions packages/next/shared/lib/loadable.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,6 @@ function createLoadableComponent(loadFn, options) {
options
)

if (!process.env.__NEXT_REACT_ROOT) {
opts.suspense = false
}

let subscription = null

function init() {
Expand Down Expand Up @@ -103,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 @@ -115,29 +112,18 @@ function createLoadableComponent(loadFn, options) {
})
}

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

const context = React.useContext(LoadableContext)
if (context && Array.isArray(opts.modules)) {
opts.modules.forEach((moduleName) => {
context(moduleName)
})
}

if (opts.suspense) {
return React.createElement(opts.loader, props)
}

const state = useSubscription(subscription)

React.useImperativeHandle(
ref,
() => ({
retry: subscription.retry,
}),
[]
)
useDynamicModules(opts.modules)

return React.useMemo(() => {
if (state.loading || state.error) {
Expand All @@ -156,6 +142,21 @@ function createLoadableComponent(loadFn, options) {
}, [props, state])
}

function useDynamicModules(modules) {
const context = React.useContext(LoadableContext)
if (context && Array.isArray(modules)) {
modules.forEach((moduleName) => {
context(moduleName)
})
}
}

function LazyImpl(props) {
useDynamicModules(opts.modules)
return React.createElement(opts.loader, props)
}

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

Expand Down
1 change: 0 additions & 1 deletion test/integration/chunking/test/index.test.js
Original file line number Diff line number Diff line change
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
1 change: 0 additions & 1 deletion test/integration/react-18/prerelease/.gitignore

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Suspense } from 'react'
import dynamic from 'next/dynamic'

const Hello = dynamic(() => import('./hello'), { suspense: true })

export default function DynamicSuspense({ suspended }) {
return (
<Suspense fallback={'loading'}>
<Hello suspended={suspended} />
</Suspense>
)
}
8 changes: 8 additions & 0 deletions test/integration/react-18/prerelease/components/hello.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import ReactDOM from 'react-dom'

export default function Hello({ suspended = false }) {
if (suspended && typeof window !== 'undefined') {
throw new Promise((resolve) => setTimeout(resolve, 1000))
}
return <p>hello {ReactDOM.version}</p>
}
3 changes: 3 additions & 0 deletions test/integration/react-18/prerelease/next.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
module.exports = {
experimental: {
reactRoot: true,
},
webpack(config) {
const { alias } = config.resolve
// FIXME: resolving react/jsx-runtime https://github.com/facebook/react/issues/20235
Expand Down

This file was deleted.

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

This file was deleted.

14 changes: 2 additions & 12 deletions test/integration/react-18/prerelease/pages/index.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@
import dynamic from 'next/dynamic'
import { Suspense } from 'react'
import Bar from './bar'

const LazyBar = dynamic(() => import('./bar'), { suspense: true })
import ReactDOM from 'react-dom'

export default function Index() {
if (typeof window !== 'undefined') {
window.didHydrate = true
}
return (
<div>
<p>Hello</p>
<Suspense fallback={'loading...'}>
<LazyBar />
</Suspense>
<Suspense fallback={'loading...'}>
<Bar />
</Suspense>
<p id="react-dom-version">{ReactDOM.version}</p>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import DynamicSuspense from '../../components/dynamic-suspense'

export default function Dynamic() {
return <DynamicSuspense suspended={false} />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import DynamicSuspense from '../../components/dynamic-suspense'

export default function Dynamic() {
return <DynamicSuspense suspended />
}
3 changes: 0 additions & 3 deletions test/integration/react-18/supported/pages/index.js

This file was deleted.

45 changes: 45 additions & 0 deletions test/integration/react-18/test/dynamic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* eslint-env jest */
import webdriver from 'next-webdriver'
import cheerio from 'cheerio'
import { check } from 'next-test-utils'

export default (context, render) => {
async function get$(path, query) {
const html = await render(path, query)
return cheerio.load(html)
}

describe('suspense:true option', () => {
it('Should render the fallback on the server side', async () => {
const $ = await get$('/suspense/fallback')
const html = $('body').html()
expect(html).toContain('loading')
expect(JSON.parse($('#__NEXT_DATA__').html()).dynamicIds).toContain(
'./components/hello.js'
)
})

it('should render the component on client side', async () => {
let browser
try {
browser = await webdriver(context.appPort, '/suspense/fallback')
await check(() => browser.elementByCss('body').text(), /hello/)
} finally {
if (browser) {
await browser.close()
}
}
})
})

describe('suspense:false option', () => {
it('Should render the content on the server side', async () => {
const $ = await get$('/suspense/content')
const html = $('body').html()
expect(html).toContain('hello')
expect(JSON.parse($('#__NEXT_DATA__').html()).dynamicIds).toContain(
'./components/hello.js'
)
})
})
}
84 changes: 61 additions & 23 deletions test/integration/react-18/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@ import { join } from 'path'
import fs from 'fs-extra'
import webdriver from 'next-webdriver'
import {
File,
findPort,
killApp,
launchApp,
nextBuild,
nextStart,
renderViaHTTP,
} from 'next-test-utils'
import dynamic from './dynamic'

jest.setTimeout(1000 * 60 * 5)

// overrides react and react-dom to v18
const nodeArgs = ['-r', join(__dirname, 'require-hook.js')]
const dirSupported = join(__dirname, '../supported')
const dirPrerelease = join(__dirname, '../prerelease')

const appDir = join(__dirname, '../prerelease')
const nextConfig = new File(join(appDir, 'next.config.js'))

const UNSUPPORTED_PRERELEASE =
"You are using an unsupported prerelease of 'react-dom'"
Expand Down Expand Up @@ -51,46 +55,63 @@ async function getDevOutput(dir) {
return stdout + stderr
}

describe('React 18 Support', () => {
describe('build', () => {
test('supported version of React', async () => {
const output = await getBuildOutput(dirSupported)
xdescribe('React 18 Support', () => {
describe('no warns with stable supported version of react-dom', () => {
beforeAll(async () => {
await fs.remove(join(appDir, 'node_modules'))
nextConfig.replace('reactRoot: true', '// reactRoot: true')
})
afterAll(() => {
nextConfig.replace('// reactRoot: true', 'reactRoot: true')
})

test('supported version of react in dev', async () => {
const output = await getDevOutput(appDir)
expect(output).not.toMatch(USING_CREATE_ROOT)
expect(output).not.toMatch(UNSUPPORTED_PRERELEASE)
})

test('prerelease version of React', async () => {
const output = await getBuildOutput(dirPrerelease)
expect(output).toMatch(USING_CREATE_ROOT)
expect(output).toMatch(UNSUPPORTED_PRERELEASE)
test('supported version of react in build', async () => {
const output = await getBuildOutput(appDir)
expect(output).not.toMatch(USING_CREATE_ROOT)
expect(output).not.toMatch(UNSUPPORTED_PRERELEASE)
})
})

describe('dev', () => {
test('supported version of React', async () => {
let output = await getDevOutput(dirSupported)
expect(output).not.toMatch(USING_CREATE_ROOT)
expect(output).not.toMatch(UNSUPPORTED_PRERELEASE)
describe('warns with stable supported version of react-dom', () => {
beforeAll(async () => {
const reactDomPkgPath = join(
appDir,
'node_modules/react-dom/package.json'
)
await fs.outputJson(reactDomPkgPath, {
name: 'react-dom',
version: '18.0.0-alpha-c76e4dbbc-20210722',
})
})
afterAll(async () => await fs.remove(join(appDir, 'node_modules')))

test('prerelease version of React', async () => {
let output = await getDevOutput(dirPrerelease)
const output = await getBuildOutput(appDir)
expect(output).toMatch(USING_CREATE_ROOT)
expect(output).toMatch(UNSUPPORTED_PRERELEASE)
})

test('prerelease version of React', async () => {
const output = await getDevOutput(appDir)
expect(output).toMatch(USING_CREATE_ROOT)
expect(output).toMatch(UNSUPPORTED_PRERELEASE)
})
})
})

describe('hydration', () => {
const appDir = join(__dirname, '../prerelease')
describe('React 18 Basic', () => {
xdescribe('hydration', () => {
let app
let appPort
beforeAll(async () => {
await fs.remove(join(appDir, '.next'))
await nextBuild(appDir, [dirPrerelease], {
nodeArgs,
stdout: true,
stderr: true,
})
await nextBuild(appDir, [appDir], { nodeArgs })
appPort = await findPort()
app = await nextStart(appDir, appPort, { nodeArgs })
})
Expand All @@ -100,6 +121,23 @@ describe('React 18 Support', () => {
it('hydrates correctly for normal page', async () => {
const browser = await webdriver(appPort, '/')
expect(await browser.eval('window.didHydrate')).toBe(true)
expect(await browser.elementById('react-dom-version').text()).toMatch(
/18/
)
})
})

describe('dynamic import', () => {
const context = {}

beforeAll(async () => {
context.appPort = await findPort()
context.server = await launchApp(appDir, context.appPort, { nodeArgs })
})
afterAll(async () => {
await killApp(context.server)
})

dynamic(context, (p, q) => renderViaHTTP(context.appPort, p, q))
})
})

0 comments on commit d673bb5

Please sign in to comment.