Skip to content

Commit 9df59c1

Browse files
authored
Dynamic component support with multiple modules (vercel#2235)
* Layout ground works for next/async * Implement the Dynamic Bundle feature. * Add some test cases. * Update README. * Implement props aware dynamic bundle API. * Update tests and README. * Add a test case for React Context support.
1 parent f36b4f9 commit 9df59c1

12 files changed

Lines changed: 437 additions & 39 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default () => (
2+
<p>Hello World 6 (imported dynamiclly) </p>
3+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default () => (
2+
<p>Hello World 7 (imported dynamiclly) </p>
3+
)
Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react'
2+
import Router from 'next/router'
23
import Header from '../components/Header'
34
import Counter from '../components/Counter'
45
import dynamic from 'next/dynamic'
@@ -22,22 +23,64 @@ const DynamicComponentWithAsyncReactor = asyncReactor(async () => {
2223

2324
const DynamicComponent5 = dynamic(import('../components/hello5'))
2425

25-
export default () => (
26-
<div>
27-
<Header />
28-
<DynamicComponent />
29-
<DynamicComponentWithCustomLoading />
30-
<DynamicComponentWithNoSSR />
31-
<DynamicComponentWithAsyncReactor />
32-
{
33-
/*
34-
Since DynamicComponent5 does not render in the client,
35-
it won't get downloaded.
36-
*/
26+
const DynamicBundle = dynamic({
27+
modules: (props) => {
28+
const components = {
29+
Hello6: import('../components/hello6')
3730
}
38-
{ React.noSuchField === true ? <DynamicComponent5 /> : null }
3931

40-
<p>HOME PAGE is here!</p>
41-
<Counter />
42-
</div>
43-
)
32+
if (props.showMore) {
33+
components.Hello7 = import('../components/hello7')
34+
}
35+
36+
return components
37+
},
38+
render: (props, { Hello6, Hello7 }) => (
39+
<div style={{padding: 10, border: '1px solid #888'}}>
40+
<Hello6 />
41+
{Hello7 ? <Hello7 /> : null}
42+
</div>
43+
)
44+
})
45+
46+
export default class Index extends React.Component {
47+
static getInitialProps ({ query }) {
48+
return { showMore: Boolean(query.showMore) }
49+
}
50+
51+
toggleShowMore () {
52+
const { showMore } = this.props
53+
if (showMore) {
54+
Router.push('/')
55+
return
56+
}
57+
58+
Router.push('/?showMore=1')
59+
}
60+
61+
render () {
62+
const { showMore } = this.props
63+
64+
return (
65+
<div>
66+
<Header />
67+
<DynamicComponent />
68+
<DynamicComponentWithCustomLoading />
69+
<DynamicComponentWithNoSSR />
70+
<DynamicComponentWithAsyncReactor />
71+
<DynamicBundle showMore={showMore} />
72+
<button onClick={() => this.toggleShowMore()}>Toggle Show More</button>
73+
{
74+
/*
75+
Since DynamicComponent5 does not render in the client,
76+
it won't get downloaded.
77+
*/
78+
}
79+
{ React.noSuchField === true ? <DynamicComponent5 /> : null }
80+
81+
<p>HOME PAGE is here!</p>
82+
<Counter />
83+
</div>
84+
)
85+
}
86+
}

lib/dynamic.js

Lines changed: 106 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,54 @@
11
import React from 'react'
22

3-
let currentChunks = []
3+
let currentChunks = new Set()
4+
5+
export default function dynamicComponent (p, o) {
6+
let promise
7+
let options
8+
9+
if (p instanceof SameLoopPromise) {
10+
promise = p
11+
options = o || {}
12+
} else {
13+
// Now we are trying to use the modules and render fields in options to load modules.
14+
if (!p.modules || !p.render) {
15+
const errorMessage = 'Options to `next/dynamic` should contains `modules` and `render` fields.'
16+
throw new Error(errorMessage)
17+
}
18+
19+
if (o) {
20+
const errorMessage = 'Include options in the first argument which contains `modules` and `render` fields.'
21+
throw new Error(errorMessage)
22+
}
23+
24+
options = p
25+
}
426

5-
export default function dynamicComponent (promise, options = {}) {
627
return class DynamicComponent extends React.Component {
728
constructor (...args) {
829
super(...args)
930

1031
this.LoadingComponent = options.loading ? options.loading : () => (<p>loading...</p>)
1132
this.ssr = options.ssr === false ? options.ssr : true
1233

13-
this.state = { AsyncComponent: null }
34+
this.state = { AsyncComponent: null, asyncElement: null }
1435
this.isServer = typeof window === 'undefined'
1536

37+
// This flag is used to load the bundle again, if needed
38+
this.loadBundleAgain = null
39+
// This flag keeps track of the whether we are loading a bundle or not.
40+
this.loadingBundle = false
41+
1642
if (this.ssr) {
43+
this.load()
44+
}
45+
}
46+
47+
load () {
48+
if (promise) {
1749
this.loadComponent()
50+
} else {
51+
this.loadBundle(this.props)
1852
}
1953
}
2054

@@ -30,33 +64,95 @@ export default function dynamicComponent (promise, options = {}) {
3064
this.setState({ AsyncComponent })
3165
} else {
3266
if (this.isServer) {
33-
currentChunks.push(AsyncComponent.__webpackChunkName)
67+
registerChunk(AsyncComponent.__webpackChunkName)
3468
}
3569
this.state.AsyncComponent = AsyncComponent
3670
}
3771
})
3872
}
3973

74+
loadBundle (props) {
75+
this.loadBundleAgain = null
76+
this.loadingBundle = true
77+
78+
// Run this for prop changes as well.
79+
const modulePromiseMap = options.modules(props)
80+
const moduleNames = Object.keys(modulePromiseMap)
81+
let remainingPromises = moduleNames.length
82+
const moduleMap = {}
83+
84+
const renderModules = () => {
85+
if (this.loadBundleAgain) {
86+
this.loadBundle(this.loadBundleAgain)
87+
return
88+
}
89+
90+
this.loadingBundle = false
91+
DynamicComponent.displayName = 'DynamicBundle'
92+
const asyncElement = options.render(props, moduleMap)
93+
if (this.mounted) {
94+
this.setState({ asyncElement })
95+
} else {
96+
this.state.asyncElement = asyncElement
97+
}
98+
}
99+
100+
const loadModule = (name) => {
101+
const promise = modulePromiseMap[name]
102+
promise.then((Component) => {
103+
if (this.isServer) {
104+
registerChunk(Component.__webpackChunkName)
105+
}
106+
moduleMap[name] = Component
107+
remainingPromises--
108+
if (remainingPromises === 0) {
109+
renderModules()
110+
}
111+
})
112+
}
113+
114+
moduleNames.forEach(loadModule)
115+
}
116+
40117
componentDidMount () {
41118
this.mounted = true
42119
if (!this.ssr) {
43-
this.loadComponent()
120+
this.load()
44121
}
45122
}
46123

124+
componentWillReceiveProps (nextProps) {
125+
if (promise) return
126+
127+
this.setState({ asyncElement: null })
128+
129+
if (this.loadingBundle) {
130+
this.loadBundleAgain = nextProps
131+
return
132+
}
133+
134+
this.loadBundle(nextProps)
135+
}
136+
47137
render () {
48-
const { AsyncComponent } = this.state
138+
const { AsyncComponent, asyncElement } = this.state
49139
const { LoadingComponent } = this
50-
if (!AsyncComponent) return (<LoadingComponent {...this.props} />)
51140

52-
return <AsyncComponent {...this.props} />
141+
if (asyncElement) return asyncElement
142+
if (AsyncComponent) return (<AsyncComponent {...this.props} />)
143+
144+
return (<LoadingComponent {...this.props} />)
53145
}
54146
}
55147
}
56148

149+
export function registerChunk (chunk) {
150+
currentChunks.add(chunk)
151+
}
152+
57153
export function flushChunks () {
58-
const chunks = currentChunks
59-
currentChunks = []
154+
const chunks = Array.from(currentChunks)
155+
currentChunks.clear()
60156
return chunks
61157
}
62158

readme.md

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -657,23 +657,33 @@ export default () => (
657657
)
658658
```
659659

660-
#### 4. With [async-reactor](https://github.com/xtuc/async-reactor)
661-
662-
> SSR support is not available here
660+
#### 4. With Multiple Modules At Once
663661

664662
```js
665-
import { asyncReactor } from 'async-reactor'
666-
const DynamicComponentWithAsyncReactor = asyncReactor(async () => {
667-
const Hello4 = await import('../components/hello4')
668-
return (<Hello4 />)
663+
import dynamic from 'next/dynamic'
664+
665+
const HelloBundle = dynamic({
666+
modules: (props) => {
667+
const components {
668+
Hello1: import('../components/hello1'),
669+
Hello2: import('../components/hello2')
670+
}
671+
672+
// Add remove components based on props
673+
674+
return components
675+
},
676+
render: (props, { Hello1, Hello2 }) => (
677+
<div>
678+
<h1>{props.title}</h1>
679+
<Hello1 />
680+
<Hello2 />
681+
</div>
682+
)
669683
})
670684

671685
export default () => (
672-
<div>
673-
<Header />
674-
<DynamicComponentWithAsyncReactor />
675-
<p>HOME PAGE is here!</p>
676-
</div>
686+
<HelloBundle title="Dynamic Bundle"/>
677687
)
678688
```
679689

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from 'react'
2+
import PropTypes from 'prop-types'
3+
4+
export default class extends React.Component {
5+
static contextTypes = {
6+
data: PropTypes.object
7+
}
8+
9+
render () {
10+
const { data } = this.context
11+
return (
12+
<div>{data.title}</div>
13+
)
14+
}
15+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default () => (
2+
<p>Hello World 2</p>
3+
)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React from 'react'
2+
import dynamic from 'next/dynamic'
3+
import Router from 'next/router'
4+
import PropTypes from 'prop-types'
5+
6+
const HelloBundle = dynamic({
7+
modules: (props) => {
8+
const components = {
9+
HelloContext: import('../../components/hello-context'),
10+
Hello1: import('../../components/hello1')
11+
}
12+
13+
if (props.showMore) {
14+
components.Hello2 = import('../../components/hello2')
15+
}
16+
17+
return components
18+
},
19+
render: (props, { HelloContext, Hello1, Hello2 }) => (
20+
<div>
21+
<h1>{props.title}</h1>
22+
<HelloContext />
23+
<Hello1 />
24+
{Hello2? <Hello2 /> : null}
25+
</div>
26+
)
27+
})
28+
29+
export default class Bundle extends React.Component {
30+
static getInitialProps ({ query }) {
31+
return { showMore: Boolean(query.showMore) }
32+
}
33+
34+
static childContextTypes = {
35+
data: PropTypes.object
36+
}
37+
38+
getChildContext () {
39+
return {
40+
data: { title: 'ZEIT Rocks' }
41+
}
42+
}
43+
44+
toggleShowMore () {
45+
if (this.props.showMore) {
46+
Router.push('/dynamic/bundle')
47+
return
48+
}
49+
50+
Router.push('/dynamic/bundle?showMore=1')
51+
}
52+
53+
render () {
54+
const { showMore } = this.props
55+
56+
return (
57+
<div>
58+
<HelloBundle showMore={showMore} title="Dynamic Bundle"/>
59+
<button
60+
id="toggle-show-more"
61+
onClick={() => this.toggleShowMore()}
62+
>
63+
Toggle Show More
64+
</button>
65+
</div>
66+
)
67+
}
68+
}

0 commit comments

Comments
 (0)