Fix error logging in getDerivedStateFromProps (#15797)
* Fix error logging in getDerivedStateFromProps * Update tests, don't log for both error boundary methods * Re-add change lost in rebase
Ricky committed
Jun 25, 2019 at 18:02 UTC
20da1dae4b9523ee94dc67797b11ec789e3acc68
4 files changed
+130
-1
packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
+33
@@ -644,6 +644,39 @@ describe('ReactErrorBoundaries', () => {
644
expect(container3.firstChild).toBe(null);
645
});
646
647
+ it('logs a single error when using error boundary', () => {
648
+ const container = document.createElement('div');
649
+ expect(() =>
650
+ ReactDOM.render(
651
+ <ErrorBoundary>
652
+ <BrokenRender />
653
+ </ErrorBoundary>,
654
+ container,
655
+ ),
656
+ ).toWarnDev('The above error occurred in the <BrokenRender> component:', {
657
+ logAllErrors: true,
658
+ });
659
+
660
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
661
+ expect(log).toEqual([
662
+ 'ErrorBoundary constructor',
663
+ 'ErrorBoundary componentWillMount',
664
+ 'ErrorBoundary render success',
665
+ 'BrokenRender constructor',
666
+ 'BrokenRender componentWillMount',
667
+ 'BrokenRender render [!]',
668
+ // Catch and render an error message
669
+ 'ErrorBoundary static getDerivedStateFromError',
670
+ 'ErrorBoundary componentWillMount',
671
+ 'ErrorBoundary render error',
672
+ 'ErrorBoundary componentDidMount',
673
+ ]);
674
+
675
+ log.length = 0;
676
+ ReactDOM.unmountComponentAtNode(container);
677
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
678
+ });
679
+
680
it('renders an error state if child throws in render', () => {
681
const container = document.createElement('div');
682
ReactDOM.render(
packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js
+91
@@ -30,6 +30,7 @@ describe('ReactLegacyErrorBoundaries', () => {
30
let BrokenComponentDidMountErrorBoundary;
31
let BrokenRender;
32
let ErrorBoundary;
33
+ let BothErrorBoundaries;
34
let ErrorMessage;
35
let NoopErrorBoundary;
36
let RetryErrorBoundary;
@@ -486,6 +487,57 @@ describe('ReactLegacyErrorBoundaries', () => {
487
},
488
};
489
490
+ BothErrorBoundaries = class extends React.Component {
491
+ constructor(props) {
492
+ super(props);
493
+ this.state = {error: null};
494
+ log.push('BothErrorBoundaries constructor');
495
+ }
496
+
497
+ static getDerivedStateFromError(error) {
498
+ log.push('BothErrorBoundaries static getDerivedStateFromError');
499
+ return {error};
500
+ }
501
+
502
+ render() {
503
+ if (this.state.error) {
504
+ log.push('BothErrorBoundaries render error');
505
+ return <div>Caught an error: {this.state.error.message}.</div>;
506
+ }
507
+ log.push('BothErrorBoundaries render success');
508
+ return <div>{this.props.children}</div>;
509
+ }
510
+
511
+ componentDidCatch(error) {
512
+ log.push('BothErrorBoundaries componentDidCatch');
513
+ this.setState({error});
514
+ }
515
+
516
+ UNSAFE_componentWillMount() {
517
+ log.push('BothErrorBoundaries componentWillMount');
518
+ }
519
+
520
+ componentDidMount() {
521
+ log.push('BothErrorBoundaries componentDidMount');
522
+ }
523
+
524
+ UNSAFE_componentWillReceiveProps() {
525
+ log.push('BothErrorBoundaries componentWillReceiveProps');
526
+ }
527
+
528
+ UNSAFE_componentWillUpdate() {
529
+ log.push('BothErrorBoundaries componentWillUpdate');
530
+ }
531
+
532
+ componentDidUpdate() {
533
+ log.push('BothErrorBoundaries componentDidUpdate');
534
+ }
535
+
536
+ componentWillUnmount() {
537
+ log.push('BothErrorBoundaries componentWillUnmount');
538
+ }
539
+ };
540
+
541
RetryErrorBoundary = class extends React.Component {
542
constructor(props) {
543
super(props);
@@ -614,6 +666,45 @@ describe('ReactLegacyErrorBoundaries', () => {
666
expect(container3.firstChild).toBe(null);
667
});
668
669
+ it('logs a single error using both error boundaries', () => {
670
+ const container = document.createElement('div');
671
+ expect(() =>
672
+ ReactDOM.render(
673
+ <BothErrorBoundaries>
674
+ <BrokenRender />
675
+ </BothErrorBoundaries>,
676
+ container,
677
+ ),
678
+ ).toWarnDev('The above error occurred in the <BrokenRender> component', {
679
+ logAllErrors: true,
680
+ });
681
+
682
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
683
+ expect(log).toEqual([
684
+ 'BothErrorBoundaries constructor',
685
+ 'BothErrorBoundaries componentWillMount',
686
+ 'BothErrorBoundaries render success',
687
+ 'BrokenRender constructor',
688
+ 'BrokenRender componentWillMount',
689
+ 'BrokenRender render [!]',
690
+ // Both getDerivedStateFromError and componentDidCatch should be called
691
+ 'BothErrorBoundaries static getDerivedStateFromError',
692
+ 'BothErrorBoundaries componentWillMount',
693
+ 'BothErrorBoundaries render error',
694
+ // Fiber mounts with null children before capturing error
695
+ 'BothErrorBoundaries componentDidMount',
696
+ // Catch and render an error message
697
+ 'BothErrorBoundaries componentDidCatch',
698
+ 'BothErrorBoundaries componentWillUpdate',
699
+ 'BothErrorBoundaries render error',
700
+ 'BothErrorBoundaries componentDidUpdate',
701
+ ]);
702
+
703
+ log.length = 0;
704
+ ReactDOM.unmountComponentAtNode(container);
705
+ expect(log).toEqual(['BothErrorBoundaries componentWillUnmount']);
706
+ });
707
+
708
it('renders an error state if child throws in render', () => {
709
const container = document.createElement('div');
710
ReactDOM.render(
packages/react-reconciler/src/ReactFiberThrow.js
+4
-1
@@ -102,6 +102,7 @@ function createClassErrorUpdate(
102
if (typeof getDerivedStateFromError === 'function') {
103
const error = errorInfo.value;
104
update.payload = () => {
105
+ logError(fiber, errorInfo);
106
return getDerivedStateFromError(error);
107
};
108
}
@@ -119,10 +120,12 @@ function createClassErrorUpdate(
120
// TODO: Warn in strict mode if getDerivedStateFromError is
121
// not defined.
122
markLegacyErrorBoundaryAsFailed(this);
123
+
124
+ // Only log here if componentDidCatch is the only error boundary method defined
125
+ logError(fiber, errorInfo);
126
}
127
const error = errorInfo.value;
128
const stack = errorInfo.stack;
125
- logError(fiber, errorInfo);
129
this.componentDidCatch(error, {
130
componentStack: stack !== null ? stack : '',
131
});
scripts/jest/matchers/toWarnDev.js
+2
@@ -38,6 +38,7 @@ const createMatcherFor = consoleMethod =>
38
}
39
40
const withoutStack = options.withoutStack;
41
+ const logAllErrors = options.logAllErrors;
42
const warningsWithoutComponentStack = [];
43
const warningsWithComponentStack = [];
44
const unexpectedWarnings = [];
@@ -58,6 +59,7 @@ const createMatcherFor = consoleMethod =>
59
// Ignore uncaught errors reported by jsdom
60
// and React addendums because they're too noisy.
61
if (
62
+ !logAllErrors &&
63
consoleMethod === 'error' &&
64
shouldIgnoreConsoleError(format, args)
65
) {