Skip to content

Commit 31bc802

Browse files
committed
Prevent an infinite loop in browsers when an error happens in resolve
Whenever an error occurs resolving a component `this.state.error = true`, however handling the error happens too late. As a result in an environment where `typeof window !== 'undefined'` i.e. the browser, this results in an infinite `render` loop. This PR hoists the error handling earlier in the `render` call. Fixes #42
1 parent 903ca10 commit 31bc802

File tree

6 files changed

+101
-17
lines changed

6 files changed

+101
-17
lines changed

commonjs/asyncComponent.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -221,19 +221,19 @@ function asyncComponent(config) {
221221
module = _state.module,
222222
error = _state.error;
223223

224+
225+
if (error) {
226+
return ErrorComponent ? _react2.default.createElement(ErrorComponent, _extends({}, this.props, { error: error })) : null;
227+
}
228+
224229
// This is as workaround for React Hot Loader support. When using
225230
// RHL the local component reference will be killed by any change
226231
// to the component, this will be our signal to know that we need to
227232
// re-resolve it.
228-
229233
if (sharedState.module == null && !this.resolving && typeof window !== 'undefined') {
230234
this.resolveModule();
231235
}
232236

233-
if (error) {
234-
return ErrorComponent ? _react2.default.createElement(ErrorComponent, _extends({}, this.props, { error: error })) : null;
235-
}
236-
237237
var Component = es6Resolve(module);
238238
return Component ? _react2.default.createElement(Component, this.props) : LoadingComponent ? _react2.default.createElement(LoadingComponent, this.props) : null;
239239
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`asyncComponent in a browser environment when an error occurs resolving a component should render the ErrorComponent 1`] = `
4+
<AsyncComponent>
5+
<ErrorComponent
6+
error={[Error: failed to resolve]}
7+
>
8+
<div>
9+
failed to resolve
10+
</div>
11+
</ErrorComponent>
12+
</AsyncComponent>
13+
`;
14+
15+
exports[`asyncComponent in a server environment when an error occurs resolving a component should render the ErrorComponent 1`] = `
16+
<AsyncComponent>
17+
<ErrorComponent
18+
error={[Error: failed to resolve]}
19+
>
20+
<div>
21+
failed to resolve
22+
</div>
23+
</ErrorComponent>
24+
</AsyncComponent>
25+
`;

src/__tests__/asyncComponent.test.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,63 @@ describe('asyncComponent', () => {
2222
setTimeout(resolve, resolveDelay + 10),
2323
).then(() => expect(setStateSpy.callCount).toEqual(1))
2424
})
25+
26+
describe('in a browser environment', () => {
27+
let oldWindow
28+
beforeAll(() => {
29+
oldWindow = window
30+
global.window = {}
31+
})
32+
afterAll(() => {
33+
global.window = oldWindow
34+
})
35+
describe('when an error occurs resolving a component', () => {
36+
it.only('should render the ErrorComponent', () => {
37+
const resolveDelay = 10
38+
const Bob = asyncComponent({
39+
resolve: () =>
40+
new Promise((resolve, reject) =>
41+
setTimeout(
42+
() => reject(new Error('failed to resolve')),
43+
resolveDelay,
44+
),
45+
),
46+
ErrorComponent: ({ error }) => <div>{error.message}</div>,
47+
})
48+
const renderWrapper = mount(<Bob />)
49+
return new Promise(resolve =>
50+
setTimeout(resolve, resolveDelay + 100),
51+
).then(() => expect(renderWrapper).toMatchSnapshot())
52+
})
53+
})
54+
})
55+
describe('in a server environment', () => {
56+
let oldWindow
57+
beforeAll(() => {
58+
oldWindow = window
59+
global.window = undefined
60+
})
61+
afterAll(() => {
62+
global.window = oldWindow
63+
})
64+
describe('when an error occurs resolving a component', () => {
65+
it('should render the ErrorComponent', () => {
66+
const resolveDelay = 10
67+
const Bob = asyncComponent({
68+
resolve: () =>
69+
new Promise((resolve, reject) =>
70+
setTimeout(
71+
() => reject(new Error('failed to resolve')),
72+
resolveDelay,
73+
),
74+
),
75+
ErrorComponent: ({ error }) => <div>{error.message}</div>,
76+
})
77+
const renderWrapper = mount(<Bob />)
78+
return new Promise(resolve =>
79+
setTimeout(resolve, resolveDelay + 50),
80+
).then(() => expect(renderWrapper).toMatchSnapshot())
81+
})
82+
})
83+
})
2584
})

src/asyncComponent.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,12 @@ function asyncComponent(config) {
176176
render() {
177177
const { module, error } = this.state
178178

179+
if (error) {
180+
return ErrorComponent ? (
181+
<ErrorComponent {...this.props} error={error} />
182+
) : null
183+
}
184+
179185
// This is as workaround for React Hot Loader support. When using
180186
// RHL the local component reference will be killed by any change
181187
// to the component, this will be our signal to know that we need to
@@ -188,12 +194,6 @@ function asyncComponent(config) {
188194
this.resolveModule()
189195
}
190196

191-
if (error) {
192-
return ErrorComponent ? (
193-
<ErrorComponent {...this.props} error={error} />
194-
) : null
195-
}
196-
197197
const Component = es6Resolve(module)
198198
return Component ? (
199199
<Component {...this.props} />

umd/react-async-component.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -482,19 +482,19 @@ function asyncComponent(config) {
482482
module = _state.module,
483483
error = _state.error;
484484

485+
486+
if (error) {
487+
return ErrorComponent ? _react2.default.createElement(ErrorComponent, _extends({}, this.props, { error: error })) : null;
488+
}
489+
485490
// This is as workaround for React Hot Loader support. When using
486491
// RHL the local component reference will be killed by any change
487492
// to the component, this will be our signal to know that we need to
488493
// re-resolve it.
489-
490494
if (sharedState.module == null && !this.resolving && typeof window !== 'undefined') {
491495
this.resolveModule();
492496
}
493497

494-
if (error) {
495-
return ErrorComponent ? _react2.default.createElement(ErrorComponent, _extends({}, this.props, { error: error })) : null;
496-
}
497-
498498
var Component = es6Resolve(module);
499499
return Component ? _react2.default.createElement(Component, this.props) : LoadingComponent ? _react2.default.createElement(LoadingComponent, this.props) : null;
500500
}

0 commit comments

Comments
 (0)