Skip to content

Commit 10f9f9e

Browse files
gaearonmadeinfree
authored andcommitted
Throw if document is missing by the time invokeGuardedCallbackDev runs (facebook#11677)
* Warn if `document` is missing by the time invokeGuardedCallback runs in DEV * Typo * Add a comment * Use invariant() instead * Create event immediately for clarity
1 parent bd61655 commit 10f9f9e

File tree

2 files changed

+55
-1
lines changed

2 files changed

+55
-1
lines changed

packages/react-dom/src/__tests__/ReactDOM-test.js

+39
Original file line numberDiff line numberDiff line change
@@ -372,4 +372,43 @@ describe('ReactDOM', () => {
372372
delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
373373
}
374374
});
375+
376+
it('throws in DEV if jsdom is destroyed by the time setState() is called', () => {
377+
spyOnDev(console, 'error');
378+
class App extends React.Component {
379+
state = {x: 1};
380+
render() {
381+
return <div />;
382+
}
383+
}
384+
const container = document.createElement('div');
385+
const instance = ReactDOM.render(<App />, container);
386+
const documentDescriptor = Object.getOwnPropertyDescriptor(
387+
global,
388+
'document',
389+
);
390+
try {
391+
// Emulate jsdom environment cleanup.
392+
// This is roughly what happens if the test finished and then
393+
// an asynchronous callback tried to setState() after this.
394+
delete global.document;
395+
const fn = () => instance.setState({x: 2});
396+
if (__DEV__) {
397+
expect(fn).toThrow(
398+
'The `document` global was defined when React was initialized, but is not ' +
399+
'defined anymore. This can happen in a test environment if a component ' +
400+
'schedules an update from an asynchronous callback, but the test has already ' +
401+
'finished running. To solve this, you can either unmount the component at ' +
402+
'the end of your test (and ensure that any asynchronous operations get ' +
403+
'canceled in `componentWillUnmount`), or you can change the test itself ' +
404+
'to be asynchronous.',
405+
);
406+
} else {
407+
expect(fn).not.toThrow();
408+
}
409+
} finally {
410+
// Don't break other tests.
411+
Object.defineProperty(global, 'document', documentDescriptor);
412+
}
413+
});
375414
});

packages/shared/ReactErrorUtils.js

+16-1
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,22 @@ if (__DEV__) {
167167
e,
168168
f,
169169
) {
170+
// If document doesn't exist we know for sure we will crash in this method
171+
// when we call document.createEvent(). However this can cause confusing
172+
// errors: https://github.com/facebookincubator/create-react-app/issues/3482
173+
// So we preemptively throw with a better message instead.
174+
invariant(
175+
typeof document !== 'undefined',
176+
'The `document` global was defined when React was initialized, but is not ' +
177+
'defined anymore. This can happen in a test environment if a component ' +
178+
'schedules an update from an asynchronous callback, but the test has already ' +
179+
'finished running. To solve this, you can either unmount the component at ' +
180+
'the end of your test (and ensure that any asynchronous operations get ' +
181+
'canceled in `componentWillUnmount`), or you can change the test itself ' +
182+
'to be asynchronous.',
183+
);
184+
const evt = document.createEvent('Event');
185+
170186
// Keeps track of whether the user-provided callback threw an error. We
171187
// set this to true at the beginning, then set it to false right after
172188
// calling the function. If the function errors, `didError` will never be
@@ -222,7 +238,6 @@ if (__DEV__) {
222238

223239
// Synchronously dispatch our fake event. If the user-provided function
224240
// errors, it will trigger our global error handler.
225-
const evt = document.createEvent('Event');
226241
evt.initEvent(evtType, false, false);
227242
fakeNode.dispatchEvent(evt);
228243

0 commit comments

Comments
 (0)