Skip to content

Commit

Permalink
override ssr option, update docs and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
huozhi committed Jul 30, 2021
1 parent d673bb5 commit 06e6480
Show file tree
Hide file tree
Showing 11 changed files with 149 additions and 62 deletions.
3 changes: 3 additions & 0 deletions docs/advanced-features/dynamic-import.md
Expand Up @@ -179,3 +179,6 @@ function Home() {

export default Home
```

If you're using `suspense: true`, `ssr` option will set to `true` to delegate rendering to React `<Suspense />`.
It's similar to using `React.lazy`, but `next/dynamic` will support mode like SSG.
47 changes: 31 additions & 16 deletions packages/next/shared/lib/dynamic.tsx
Expand Up @@ -67,8 +67,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 @@ -115,36 +113,53 @@ export default function dynamic<P = {}>(
loadableOptions = { ...loadableOptions, ...options }

if (!process.env.__NEXT_REACT_ROOT) {
if (
process.env.NODE_ENV !== 'production' &&
loadableOptions.suspense &&
!isServerSide
) {
console.warn(
`Enable experimental.reactRoot or use React version above 18 to use suspense option`
)
}
loadableOptions.suspense = false
}
// If suspense is enabled, delegate rendering to suspense
if (loadableOptions.suspense) {
loadableOptions.ssr = true
}

if (
typeof loadableOptions.loader === 'function' &&
loadableOptions.suspense
) {
const { suspense, ssr, loader, loadableGenerated } = loadableOptions
// client side rendering
const csr = ssr === false
delete loadableOptions.ssr
delete loadableOptions.loadableGenerated

if (typeof loadableOptions.loader === 'function' && suspense) {
// If `suspense = true` and `ssr = false`:
// render empty on server side;
// render lazy component on client side.
if (csr && isServerSide) {
return () => null
}
loadableOptions.loader = React.lazy(
loadableOptions.loader as () => Promise<{
loader as () => Promise<{
default: React.ComponentType<P>
}>
)
}

// coming from build/babel/plugins/react-loadable-plugin.js
if (loadableOptions.loadableGenerated) {
if (loadableGenerated) {
loadableOptions = {
...loadableOptions,
...loadableOptions.loadableGenerated,
...loadableGenerated,
}
delete loadableOptions.loadableGenerated
}

// support for disabling server side rendering, eg: dynamic(import('../hello-world'), {ssr: false})
if (typeof loadableOptions.ssr === 'boolean') {
if (!loadableOptions.ssr) {
delete loadableOptions.ssr
return noSSR(loadableFn, loadableOptions)
}
delete loadableOptions.ssr
if (csr && !suspense) {
return noSSR(loadableFn, loadableOptions)
}

return loadableFn(loadableOptions)
Expand Down
22 changes: 11 additions & 11 deletions packages/next/shared/lib/loadable.js
Expand Up @@ -112,6 +112,15 @@ function createLoadableComponent(loadFn, options) {
})
}

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

function LoadableImpl(props, ref) {
init()

Expand Down Expand Up @@ -142,18 +151,9 @@ 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) {
function LazyImpl(props, ref) {
useDynamicModules(opts.modules)
return React.createElement(opts.loader, props)
return React.createElement(opts.loader, { ...props, ref })
}

const LoadableComponent = opts.suspense ? LazyImpl : LoadableImpl
Expand Down
@@ -1,12 +1,15 @@
import { Suspense } from 'react'
import dynamic from 'next/dynamic'

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

export default function DynamicSuspense({ suspended }) {
export default function SuspenseNoSSR({ thrown }) {
return (
<Suspense fallback={'loading'}>
<Hello suspended={suspended} />
<Hello thrown={thrown} />
</Suspense>
)
}
8 changes: 5 additions & 3 deletions test/integration/react-18/prerelease/components/hello.js
@@ -1,8 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom'

export default function Hello({ suspended = false }) {
if (suspended && typeof window !== 'undefined') {
throw new Promise((resolve) => setTimeout(resolve, 1000))
export default function Hello({ thrown = false }) {
// only throw on server side render
if (thrown && typeof window === 'undefined') {
throw new Promise((resolve) => setTimeout(resolve, 500))
}
return <p>hello {ReactDOM.version}</p>
}

This file was deleted.

This file was deleted.

@@ -0,0 +1,5 @@
import DynamicSuspense from '../../components/dynamic-suspense'

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

export default function Thrown() {
return <DynamicSuspense thrown />
}
96 changes: 80 additions & 16 deletions test/integration/react-18/test/dynamic.js
@@ -1,7 +1,31 @@
/* eslint-env jest */
import { join } from 'path'
import webdriver from 'next-webdriver'
import cheerio from 'cheerio'
import { check } from 'next-test-utils'
import { File } from 'next-test-utils'

const appDir = join(__dirname, '../prerelease')
const page = new File(join(appDir, 'components/dynamic-suspense.js'))

function writeComponent({ ssr = false, suspense = false }) {
const content = `import { Suspense } from 'react'
import dynamic from 'next/dynamic'
const Hello = dynamic(() => import('./hello'), {
ssr: ${ssr},
suspense: ${suspense},
})
export default function SuspenseNoSSR({ thrown }) {
return (
<Suspense fallback={'loading'}>
<Hello thrown={thrown} />
</Suspense>
)
}`
page.write(content)
}

export default (context, render) => {
async function get$(path, query) {
Expand All @@ -10,36 +34,76 @@ export default (context, render) => {
}

describe('suspense:true option', () => {
it('Should render the fallback on the server side', async () => {
const $ = await get$('/suspense/fallback')
describe('promise is thrown on server side', () => {
beforeAll(() => {
writeComponent({ suspense: true })
})

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

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

describe('promise is not thrown on server side', () => {
it('should render fallback on server side', async () => {
const $ = await get$('/suspense/no-thrown')
const text = $('#__next').text()
expect(text).toBe('loading')
})

it('should hydrate on client side', async () => {
let browser
try {
browser = await webdriver(context.appPort, '/suspense/no-thrown')
await check(() => browser.elementByCss('body').text(), /hello 18/)
} finally {
if (browser) {
await browser.close()
}
}
})
})
})

describe('suspense:false option', () => {
it('should render the fallback on server side', async () => {
writeComponent({ suspense: false, ssr: false })
const $ = await get$('/suspense/thrown')
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 () => {
it('should hydrate suspenses on client side with ssr disabled', async () => {
writeComponent({ suspense: false, ssr: false })
let browser
try {
browser = await webdriver(context.appPort, '/suspense/fallback')
await check(() => browser.elementByCss('body').text(), /hello/)
browser = await webdriver(context.appPort, '/suspense/no-thrown')
await check(() => browser.elementByCss('body').text(), /hello 18/)
} 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'
)
})
})
}
6 changes: 3 additions & 3 deletions test/integration/react-18/test/index.test.js
Expand Up @@ -55,7 +55,7 @@ async function getDevOutput(dir) {
return stdout + stderr
}

xdescribe('React 18 Support', () => {
describe('React 18 Support', () => {
describe('no warns with stable supported version of react-dom', () => {
beforeAll(async () => {
await fs.remove(join(appDir, 'node_modules'))
Expand Down Expand Up @@ -106,7 +106,7 @@ xdescribe('React 18 Support', () => {
})

describe('React 18 Basic', () => {
xdescribe('hydration', () => {
describe('Basic hydration', () => {
let app
let appPort
beforeAll(async () => {
Expand All @@ -127,7 +127,7 @@ describe('React 18 Basic', () => {
})
})

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

beforeAll(async () => {
Expand Down

0 comments on commit 06e6480

Please sign in to comment.