Skip to content

Commit 2f91394

Browse files
committed
Adds the capability to configure your async components to promote efficient server side rendering. Please see the docs on the ssrMode configuration property. This property replaces the removed defer configuration property.
1 parent 96a04bb commit 2f91394

File tree

4 files changed

+152
-32
lines changed

4 files changed

+152
-32
lines changed

src/__tests__/__snapshots__/integration.test.js.snap

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,37 @@ exports[`integration works 1`] = `
1010
<AsyncBob>
1111
<Bob>
1212
<div>
13-
<h1>
14-
bob
15-
</h1>
1613
<div>
1714
<AsyncBobTwo>
1815
<Bob>
1916
<div>
20-
<h1>
21-
bob
22-
</h1>
2317
<span>
24-
Hello
18+
In Render.
2519
</span>
2620
</div>
2721
</Bob>
2822
</AsyncBobTwo>
2923
<DeferredAsyncBob />
24+
<BoundaryAsyncBob>
25+
<Bob>
26+
<div>
27+
<span>
28+
In Boundary but outside an AsyncComponent, server render me!
29+
</span>
30+
<AsyncBobThree />
31+
</div>
32+
</Bob>
33+
</BoundaryAsyncBob>
3034
</div>
3135
</div>
3236
</Bob>
3337
</AsyncBob>
3438
</AsyncComponentProvider>
3539
`;
3640

37-
exports[`integration works 2`] = `
41+
exports[`integration works 2`] = `"<div data-reactroot=\"\" data-reactid=\"1\" data-react-checksum=\"-636787408\"><div data-reactid=\"2\"><div data-reactid=\"3\"><span data-reactid=\"4\">In Render.</span></div><!-- react-empty: 5 --><div data-reactid=\"6\"><span data-reactid=\"7\">In Boundary but outside an AsyncComponent, server render me!</span><!-- react-empty: 8 --></div></div></div>"`;
42+
43+
exports[`integration works 3`] = `
3844
<AsyncComponentProvider
3945
execContext={
4046
Object {
@@ -46,23 +52,83 @@ exports[`integration works 2`] = `
4652
<AsyncBob>
4753
<Bob>
4854
<div>
49-
<h1>
50-
bob
51-
</h1>
5255
<div>
5356
<AsyncBobTwo>
5457
<Bob>
5558
<div>
56-
<h1>
57-
bob
58-
</h1>
5959
<span>
60-
Hello
60+
In Render.
6161
</span>
6262
</div>
6363
</Bob>
6464
</AsyncBobTwo>
6565
<DeferredAsyncBob />
66+
<BoundaryAsyncBob>
67+
<Bob>
68+
<div>
69+
<span>
70+
In Boundary but outside an AsyncComponent, server render me!
71+
</span>
72+
<AsyncBobThree />
73+
</div>
74+
</Bob>
75+
</BoundaryAsyncBob>
76+
</div>
77+
</div>
78+
</Bob>
79+
</AsyncBob>
80+
</AsyncComponentProvider>
81+
`;
82+
83+
exports[`integration works 4`] = `
84+
<AsyncComponentProvider
85+
execContext={
86+
Object {
87+
"getComponent": [Function],
88+
"getResolved": [Function],
89+
"registerComponent": [Function],
90+
}
91+
}>
92+
<AsyncBob>
93+
<Bob>
94+
<div>
95+
<div>
96+
<AsyncBobTwo>
97+
<Bob>
98+
<div>
99+
<span>
100+
In Render.
101+
</span>
102+
</div>
103+
</Bob>
104+
</AsyncBobTwo>
105+
<DeferredAsyncBob>
106+
<Bob>
107+
<div>
108+
<span>
109+
In Defer.
110+
</span>
111+
</div>
112+
</Bob>
113+
</DeferredAsyncBob>
114+
<BoundaryAsyncBob>
115+
<Bob>
116+
<div>
117+
<span>
118+
In Boundary but outside an AsyncComponent, server render me!
119+
</span>
120+
<AsyncBobThree>
121+
<Bob>
122+
<div>
123+
<span>
124+
In Boundary - Do not server render me!
125+
</span>
126+
</div>
127+
</Bob>
128+
</AsyncBobThree>
129+
</div>
130+
</Bob>
131+
</BoundaryAsyncBob>
66132
</div>
67133
</div>
68134
</Bob>

src/__tests__/integration.test.js

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,7 @@ import { createAsyncComponent, withAsyncComponents } from '../';
77
import { STATE_IDENTIFIER } from '../constants';
88

99
function Bob({ children }) {
10-
return (
11-
<div>
12-
<h1>bob</h1>
13-
{children}
14-
</div>
15-
);
10+
return (<div>{children}</div>);
1611
}
1712
Bob.propTypes = { children: React.PropTypes.node };
1813
Bob.defaultProps = { children: null };
@@ -27,21 +22,38 @@ const AsyncBobTwo = createAsyncComponent({
2722
name: 'AsyncBobTwo',
2823
});
2924

25+
const AsyncBobThree = createAsyncComponent({
26+
resolve: () => new Promise(resolve => setTimeout(() => resolve(Bob), 10)),
27+
name: 'AsyncBobThree',
28+
});
29+
3030
const DeferredAsyncBob = createAsyncComponent({
3131
resolve: () => new Promise(resolve => setTimeout(() => resolve(Bob), 10)),
32-
defer: true,
32+
ssrMode: 'defer',
3333
name: 'DeferredAsyncBob',
3434
});
3535

36+
const BoundaryAsyncBob = createAsyncComponent({
37+
resolve: () => new Promise(resolve => setTimeout(() => resolve(Bob), 10)),
38+
ssrMode: 'boundary',
39+
name: 'BoundaryAsyncBob',
40+
});
41+
3642
const app = (
3743
<AsyncBob>
3844
<div>
3945
<AsyncBobTwo>
40-
<span>Hello</span>
46+
<span>In Render.</span>
4147
</AsyncBobTwo>
4248
<DeferredAsyncBob>
43-
<span>World!</span>
49+
<span>In Defer.</span>
4450
</DeferredAsyncBob>
51+
<BoundaryAsyncBob>
52+
<span>In Boundary but outside an AsyncComponent, server render me!</span>
53+
<AsyncBobThree>
54+
<span>In Boundary - Do not server render me!</span>
55+
</AsyncBobThree>
56+
</BoundaryAsyncBob>
4557
</div>
4658
</AsyncBob>
4759
);
@@ -56,17 +68,29 @@ describe('integration', () => {
5668
withAsyncComponents(app)
5769
.then(({ appWithAsyncComponents, state, STATE_IDENTIFIER: STATE_ID }) => {
5870
expect(mount(appWithAsyncComponents)).toMatchSnapshot();
71+
const serverString = renderToString(appWithAsyncComponents);
72+
expect(serverString).toMatchSnapshot();
5973
// Attach the state to the "window" for the client
6074
global.window[STATE_ID] = state;
61-
return renderToString(appWithAsyncComponents);
75+
return serverString;
6276
})
6377
.then(serverHTML =>
6478
// "Client" side render...
6579
withAsyncComponents(app)
6680
.then(({ appWithAsyncComponents }) => {
67-
expect(mount(appWithAsyncComponents)).toMatchSnapshot();
81+
const clientRenderWrapper = mount(appWithAsyncComponents);
82+
expect(clientRenderWrapper).toMatchSnapshot();
6883
expect(renderToString(appWithAsyncComponents)).toEqual(serverHTML);
69-
}),
84+
return clientRenderWrapper;
85+
})
86+
// Now give the client side components time to resolve
87+
.then(clientRenderWrapper => new Promise(resolve =>
88+
setTimeout(() => resolve(clientRenderWrapper), 100),
89+
))
90+
// Now a full render should have occured on client
91+
.then(clientRenderWrapper =>
92+
expect(clientRenderWrapper).toMatchSnapshot(),
93+
),
7094
),
7195
);
7296
});

src/createAsyncComponent.js

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@ import React from 'react';
33
// Duck type promise check.
44
const isPromise = x => typeof x === 'object' && typeof x.then === 'function';
55

6+
const validSSRModes = ['render', 'defer', 'boundary'];
7+
68
function createAsyncComponent(args) {
79
const {
810
name,
911
resolve,
1012
es6Aware = true,
11-
defer = false,
13+
ssrMode = 'render',
1214
Loading,
1315
} = args;
1416

17+
if (validSSRModes.indexOf(ssrMode) === -1) {
18+
throw new Error('Invalid ssrMode provided to createAsyncComponent');
19+
}
20+
1521
let id = null;
1622

1723
// Takes the given module and if it has a ".default" the ".default" will
@@ -36,10 +42,12 @@ function createAsyncComponent(args) {
3642
constructor(props, context) {
3743
super(props);
3844

45+
const { asyncComponents, asyncComponentsAncestor } = context;
46+
3947
this.state = { Component: null };
4048

41-
if (context.asyncComponents) {
42-
const { asyncComponents: { nextId, getComponent } } = context;
49+
if (asyncComponents) {
50+
const { nextId, getComponent } = asyncComponents;
4351
if (!id) {
4452
id = nextId();
4553
}
@@ -49,13 +57,25 @@ function createAsyncComponent(args) {
4957
} else {
5058
this.getAsyncComponentData = () => ({
5159
id,
52-
defer,
60+
defer: ssrMode === 'defer'
61+
|| (asyncComponentsAncestor && asyncComponentsAncestor.isBoundary),
5362
getResolver,
5463
});
5564
}
5665
}
5766
}
5867

68+
getChildContext() {
69+
if (ssrMode !== 'boundary') {
70+
return undefined;
71+
}
72+
return {
73+
asyncComponentsAncestor: {
74+
isBoundary: true,
75+
},
76+
};
77+
}
78+
5979
componentDidMount() {
6080
if (!this.state.Component) {
6181
this.resolveComponent(this.props);
@@ -89,6 +109,12 @@ function createAsyncComponent(args) {
89109
}
90110
}
91111

112+
AsyncComponent.childContextTypes = {
113+
asyncComponentsAncestor: React.PropTypes.shape({
114+
isBoundary: React.PropTypes.bool,
115+
}),
116+
};
117+
92118
AsyncComponent.contextTypes = {
93119
asyncComponents: React.PropTypes.shape({
94120
nextId: React.PropTypes.func.isRequired,

src/withAsyncComponents.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ export default function withAsyncComponents(app : React$Element) {
6060
}
6161

6262
const resolver = getResolver().then(C => execContext.registerComponent(id, C));
63-
resolvers.push({ resolver, element, context });
63+
resolvers.push({
64+
resolver,
65+
element,
66+
context: Object.assign(context, { ASYNC_WALKER_BOUNDARY: true }),
67+
});
6468
return false;
6569
}
6670
return undefined;

0 commit comments

Comments
 (0)