diff --git a/packages/react-dom/src/__tests__/ReactDOM-test.js b/packages/react-dom/src/__tests__/ReactDOM-test.js
index dde2608bfa3bd..f10ebfb6dbcdb 100644
--- a/packages/react-dom/src/__tests__/ReactDOM-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOM-test.js
@@ -372,4 +372,43 @@ describe('ReactDOM', () => {
delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
}
});
+
+ it('throws in DEV if jsdom is destroyed by the time setState() is called', () => {
+ spyOnDev(console, 'error');
+ class App extends React.Component {
+ state = {x: 1};
+ render() {
+ return
;
+ }
+ }
+ const container = document.createElement('div');
+ const instance = ReactDOM.render(, container);
+ const documentDescriptor = Object.getOwnPropertyDescriptor(
+ global,
+ 'document',
+ );
+ try {
+ // Emulate jsdom environment cleanup.
+ // This is roughly what happens if the test finished and then
+ // an asynchronous callback tried to setState() after this.
+ delete global.document;
+ const fn = () => instance.setState({x: 2});
+ if (__DEV__) {
+ expect(fn).toThrow(
+ 'The `document` global was defined when React was initialized, but is not ' +
+ 'defined anymore. This can happen in a test environment if a component ' +
+ 'schedules an update from an asynchronous callback, but the test has already ' +
+ 'finished running. To solve this, you can either unmount the component at ' +
+ 'the end of your test (and ensure that any asynchronous operations get ' +
+ 'canceled in `componentWillUnmount`), or you can change the test itself ' +
+ 'to be asynchronous.',
+ );
+ } else {
+ expect(fn).not.toThrow();
+ }
+ } finally {
+ // Don't break other tests.
+ Object.defineProperty(global, 'document', documentDescriptor);
+ }
+ });
});
diff --git a/packages/shared/ReactErrorUtils.js b/packages/shared/ReactErrorUtils.js
index cc59ee6fb4bc5..cbbfc81652bec 100644
--- a/packages/shared/ReactErrorUtils.js
+++ b/packages/shared/ReactErrorUtils.js
@@ -167,6 +167,22 @@ if (__DEV__) {
e,
f,
) {
+ // If document doesn't exist we know for sure we will crash in this method
+ // when we call document.createEvent(). However this can cause confusing
+ // errors: https://github.com/facebookincubator/create-react-app/issues/3482
+ // So we preemptively throw with a better message instead.
+ invariant(
+ typeof document !== 'undefined',
+ 'The `document` global was defined when React was initialized, but is not ' +
+ 'defined anymore. This can happen in a test environment if a component ' +
+ 'schedules an update from an asynchronous callback, but the test has already ' +
+ 'finished running. To solve this, you can either unmount the component at ' +
+ 'the end of your test (and ensure that any asynchronous operations get ' +
+ 'canceled in `componentWillUnmount`), or you can change the test itself ' +
+ 'to be asynchronous.',
+ );
+ const evt = document.createEvent('Event');
+
// Keeps track of whether the user-provided callback threw an error. We
// set this to true at the beginning, then set it to false right after
// calling the function. If the function errors, `didError` will never be
@@ -222,7 +238,6 @@ if (__DEV__) {
// Synchronously dispatch our fake event. If the user-provided function
// errors, it will trigger our global error handler.
- const evt = document.createEvent('Event');
evt.initEvent(evtType, false, false);
fakeNode.dispatchEvent(evt);