Enable getDerivedStateFromError (#13746)
* Removed the enableGetDerivedStateFromCatch feature flag (aka permanently enabled the feature) * Forked/copied ReactErrorBoundaries to ReactLegacyErrorBoundaries for testing componentDidCatch * Updated error boundaries tests to apply to getDerivedStateFromCatch * Renamed getDerivedStateFromCatch -> getDerivedStateFromError * Warn if boundary with only componentDidCatch swallows error * Fixed a subtle reconciliation bug with render phase error boundary
Brian Vaughn committed
Sep 28, 2018 at 13:05 UTC
806eebdaeec5a5b0e4e5df799bd98eb5f288bba5
25 files changed
+2639
-242
packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
+185
-157
@@ -288,9 +288,11 @@ describe('ReactErrorBoundaries', () => {
288
componentWillUnmount() {
289
log.push('BrokenComponentWillMountErrorBoundary componentWillUnmount');
290
}
291
- componentDidCatch(error) {
292
- log.push('BrokenComponentWillMountErrorBoundary componentDidCatch');
293
- this.setState({error});
291
+ static getDerivedStateFromError(error) {
292
+ log.push(
293
+ 'BrokenComponentWillMountErrorBoundary static getDerivedStateFromError',
294
+ );
295
+ return {error};
296
}
297
};
298
@@ -318,9 +320,11 @@ describe('ReactErrorBoundaries', () => {
320
componentWillUnmount() {
321
log.push('BrokenComponentDidMountErrorBoundary componentWillUnmount');
322
}
321
- componentDidCatch(error) {
322
- log.push('BrokenComponentDidMountErrorBoundary componentDidCatch');
323
- this.setState({error});
323
+ static getDerivedStateFromError(error) {
324
+ log.push(
325
+ 'BrokenComponentDidMountErrorBoundary static getDerivedStateFromError',
326
+ );
327
+ return {error};
328
}
329
};
330
@@ -347,9 +351,9 @@ describe('ReactErrorBoundaries', () => {
351
componentWillUnmount() {
352
log.push('BrokenRenderErrorBoundary componentWillUnmount');
353
}
350
- componentDidCatch(error) {
351
- log.push('BrokenRenderErrorBoundary componentDidCatch');
352
- this.setState({error});
354
+ static getDerivedStateFromError(error) {
355
+ log.push('BrokenRenderErrorBoundary static getDerivedStateFromError');
356
+ return {error};
357
}
358
};
359
@@ -400,8 +404,8 @@ describe('ReactErrorBoundaries', () => {
404
componentWillUnmount() {
405
log.push('NoopErrorBoundary componentWillUnmount');
406
}
403
- componentDidCatch() {
404
- log.push('NoopErrorBoundary componentDidCatch');
407
+ static getDerivedStateFromError() {
408
+ log.push('NoopErrorBoundary static getDerivedStateFromError');
409
}
410
};
411
@@ -451,9 +455,9 @@ describe('ReactErrorBoundaries', () => {
455
log.push(`${this.props.logName} render success`);
456
return <div>{this.props.children}</div>;
457
}
454
- componentDidCatch(error) {
455
- log.push(`${this.props.logName} componentDidCatch`);
456
- this.setState({error});
458
+ static getDerivedStateFromError(error) {
459
+ log.push('ErrorBoundary static getDerivedStateFromError');
460
+ return {error};
461
}
462
UNSAFE_componentWillMount() {
463
log.push(`${this.props.logName} componentWillMount`);
@@ -503,10 +507,10 @@ describe('ReactErrorBoundaries', () => {
507
componentWillUnmount() {
508
log.push('RetryErrorBoundary componentWillUnmount');
509
}
506
- componentDidCatch(error) {
507
- log.push('RetryErrorBoundary componentDidCatch [!]');
510
+ static getDerivedStateFromError(error) {
511
+ log.push('RetryErrorBoundary static getDerivedStateFromError [!]');
512
// In Fiber, calling setState() (and failing) is treated as a rethrow.
509
- this.setState({});
513
+ return {};
514
}
515
};
516
@@ -629,13 +633,11 @@ describe('ReactErrorBoundaries', () => {
633
'BrokenRender constructor',
634
'BrokenRender componentWillMount',
635
'BrokenRender render [!]',
632
- // Fiber mounts with null children before capturing error
633
- 'ErrorBoundary componentDidMount',
636
// Catch and render an error message
635
- 'ErrorBoundary componentDidCatch',
636
- 'ErrorBoundary componentWillUpdate',
637
+ 'ErrorBoundary static getDerivedStateFromError',
638
+ 'ErrorBoundary componentWillMount',
639
'ErrorBoundary render error',
638
- 'ErrorBoundary componentDidUpdate',
640
+ 'ErrorBoundary componentDidMount',
641
]);
642
643
log.length = 0;
@@ -657,13 +659,11 @@ describe('ReactErrorBoundaries', () => {
659
'ErrorBoundary componentWillMount',
660
'ErrorBoundary render success',
661
'BrokenConstructor constructor [!]',
660
- // Fiber mounts with null children before capturing error
661
- 'ErrorBoundary componentDidMount',
662
// Catch and render an error message
663
- 'ErrorBoundary componentDidCatch',
664
- 'ErrorBoundary componentWillUpdate',
663
+ 'ErrorBoundary static getDerivedStateFromError',
664
+ 'ErrorBoundary componentWillMount',
665
'ErrorBoundary render error',
666
- 'ErrorBoundary componentDidUpdate',
666
+ 'ErrorBoundary componentDidMount',
667
]);
668
669
log.length = 0;
@@ -686,11 +686,11 @@ describe('ReactErrorBoundaries', () => {
686
'ErrorBoundary render success',
687
'BrokenComponentWillMount constructor',
688
'BrokenComponentWillMount componentWillMount [!]',
689
- 'ErrorBoundary componentDidMount',
690
- 'ErrorBoundary componentDidCatch',
691
- 'ErrorBoundary componentWillUpdate',
689
+ // Catch and render an error message
690
+ 'ErrorBoundary static getDerivedStateFromError',
691
+ 'ErrorBoundary componentWillMount',
692
'ErrorBoundary render error',
693
- 'ErrorBoundary componentDidUpdate',
693
+ 'ErrorBoundary componentDidMount',
694
]);
695
696
log.length = 0;
@@ -769,15 +769,14 @@ describe('ReactErrorBoundaries', () => {
769
'BrokenRender constructor',
770
'BrokenRender componentWillMount',
771
'BrokenRender render [!]',
772
- 'ErrorBoundary componentDidMount',
773
- 'ErrorBoundary componentDidCatch',
774
- 'ErrorBoundary componentWillUpdate',
772
+ 'ErrorBoundary static getDerivedStateFromError',
773
+ 'ErrorBoundary componentWillMount',
774
'ErrorBoundary render error',
775
'ErrorMessage constructor',
776
'ErrorMessage componentWillMount',
777
'ErrorMessage render',
778
'ErrorMessage componentDidMount',
780
- 'ErrorBoundary componentDidUpdate',
779
+ 'ErrorBoundary componentDidMount',
780
]);
781
782
log.length = 0;
@@ -809,22 +808,18 @@ describe('ReactErrorBoundaries', () => {
808
'BrokenRender constructor',
809
'BrokenRender componentWillMount',
810
'BrokenRender render [!]',
812
- // In Fiber, failed error boundaries render null before attempting to recover
813
- 'RetryErrorBoundary componentDidMount',
814
- 'RetryErrorBoundary componentDidCatch [!]',
815
- 'ErrorBoundary componentDidMount',
811
// Retry
812
+ 'RetryErrorBoundary static getDerivedStateFromError [!]',
813
+ 'RetryErrorBoundary componentWillMount',
814
'RetryErrorBoundary render',
815
'BrokenRender constructor',
816
'BrokenRender componentWillMount',
817
'BrokenRender render [!]',
818
// This time, the error propagates to the higher boundary
822
- 'RetryErrorBoundary componentWillUnmount',
823
- 'ErrorBoundary componentDidCatch',
824
- // Render the error
825
- 'ErrorBoundary componentWillUpdate',
819
+ 'ErrorBoundary static getDerivedStateFromError',
820
+ 'ErrorBoundary componentWillMount',
821
'ErrorBoundary render error',
827
- 'ErrorBoundary componentDidUpdate',
822
+ 'ErrorBoundary componentDidMount',
823
]);
824
825
log.length = 0;
@@ -848,11 +843,10 @@ describe('ReactErrorBoundaries', () => {
843
'BrokenComponentWillMountErrorBoundary constructor',
844
'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
845
// The error propagates to the higher boundary
851
- 'ErrorBoundary componentDidMount',
852
- 'ErrorBoundary componentDidCatch',
853
- 'ErrorBoundary componentWillUpdate',
846
+ 'ErrorBoundary static getDerivedStateFromError',
847
+ 'ErrorBoundary componentWillMount',
848
'ErrorBoundary render error',
855
- 'ErrorBoundary componentDidUpdate',
849
+ 'ErrorBoundary componentDidMount',
850
]);
851
852
log.length = 0;
@@ -881,21 +875,15 @@ describe('ReactErrorBoundaries', () => {
875
'BrokenRender constructor',
876
'BrokenRender componentWillMount',
877
'BrokenRender render [!]',
884
- // The first error boundary catches the error
885
- // It adjusts state but throws displaying the message
886
- // Finish mounting with null children
887
- 'BrokenRenderErrorBoundary componentDidMount',
878
// Attempt to handle the error
889
- 'BrokenRenderErrorBoundary componentDidCatch',
890
- 'ErrorBoundary componentDidMount',
879
+ 'BrokenRenderErrorBoundary static getDerivedStateFromError',
880
+ 'BrokenRenderErrorBoundary componentWillMount',
881
'BrokenRenderErrorBoundary render error [!]',
892
- // Boundary fails with new error, propagate to next boundary
893
- 'BrokenRenderErrorBoundary componentWillUnmount',
882
// Attempt to handle the error again
895
- 'ErrorBoundary componentDidCatch',
896
- 'ErrorBoundary componentWillUpdate',
883
+ 'ErrorBoundary static getDerivedStateFromError',
884
+ 'ErrorBoundary componentWillMount',
885
'ErrorBoundary render error',
898
- 'ErrorBoundary componentDidUpdate',
886
+ 'ErrorBoundary componentDidMount',
887
]);
888
889
log.length = 0;
@@ -930,14 +918,11 @@ describe('ReactErrorBoundaries', () => {
918
'Normal constructor',
919
'Normal componentWillMount',
920
'Normal render',
933
- // Finish mounting with null children
934
- 'ErrorBoundary componentDidMount',
921
// Handle the error
936
- 'ErrorBoundary componentDidCatch',
937
- // Render the error message
938
- 'ErrorBoundary componentWillUpdate',
922
+ 'ErrorBoundary static getDerivedStateFromError',
923
+ 'ErrorBoundary componentWillMount',
924
'ErrorBoundary render error',
940
- 'ErrorBoundary componentDidUpdate',
925
+ 'ErrorBoundary componentDidMount',
926
]);
927
928
log.length = 0;
@@ -969,16 +954,12 @@ describe('ReactErrorBoundaries', () => {
954
'BrokenRender constructor',
955
'BrokenRender componentWillMount',
956
'BrokenRender render [!]',
972
- // Handle error:
973
- // Finish mounting with null children
974
- 'ErrorBoundary componentDidMount',
957
// Handle the error
976
- 'ErrorBoundary componentDidCatch',
977
- // Render the error message
978
- 'ErrorBoundary componentWillUpdate',
958
+ 'ErrorBoundary static getDerivedStateFromError',
959
+ 'ErrorBoundary componentWillMount',
960
'ErrorBoundary render error',
961
'Error message ref is set to [object HTMLDivElement]',
981
- 'ErrorBoundary componentDidUpdate',
962
+ 'ErrorBoundary componentDidMount',
963
]);
964
965
log.length = 0;
@@ -1009,15 +990,11 @@ describe('ReactErrorBoundaries', () => {
990
'BrokenRender constructor',
991
'BrokenRender componentWillMount',
992
'BrokenRender render [!]',
1012
- // Handle error:
1013
- // Finish mounting with null children
1014
- 'ErrorBoundary componentDidMount',
993
// Handle the error
1016
- 'ErrorBoundary componentDidCatch',
1017
- // Render the error message
1018
- 'ErrorBoundary componentWillUpdate',
994
+ 'ErrorBoundary static getDerivedStateFromError',
995
+ 'ErrorBoundary componentWillMount',
996
'ErrorBoundary render error',
1020
- 'ErrorBoundary componentDidUpdate',
997
+ 'ErrorBoundary componentDidMount',
998
]);
999
expect(errorMessageRef.current.toString()).toEqual(
1000
'[object HTMLDivElement]',
@@ -1058,7 +1035,6 @@ describe('ReactErrorBoundaries', () => {
1035
</ErrorBoundary>,
1036
container,
1037
);
1061
-
1038
log.length = 0;
1039
ReactDOM.render(
1040
<ErrorBoundary>
@@ -1082,14 +1058,12 @@ describe('ReactErrorBoundaries', () => {
1058
'Normal2 render',
1059
// BrokenConstructor will abort rendering:
1060
'BrokenConstructor constructor [!]',
1085
- // Finish updating with null children
1086
- 'Normal componentWillUnmount',
1087
- 'ErrorBoundary componentDidUpdate',
1061
// Handle the error
1089
- 'ErrorBoundary componentDidCatch',
1062
+ 'ErrorBoundary static getDerivedStateFromError',
1063
// Render the error message
1064
'ErrorBoundary componentWillUpdate',
1065
'ErrorBoundary render error',
1066
+ 'Normal componentWillUnmount',
1067
'ErrorBoundary componentDidUpdate',
1068
]);
1069
@@ -1131,14 +1105,12 @@ describe('ReactErrorBoundaries', () => {
1105
// BrokenComponentWillMount will abort rendering:
1106
'BrokenComponentWillMount constructor',
1107
'BrokenComponentWillMount componentWillMount [!]',
1134
- // Finish updating with null children
1135
- 'Normal componentWillUnmount',
1136
- 'ErrorBoundary componentDidUpdate',
1108
// Handle the error
1138
- 'ErrorBoundary componentDidCatch',
1109
+ 'ErrorBoundary static getDerivedStateFromError',
1110
// Render the error message
1111
'ErrorBoundary componentWillUpdate',
1112
'ErrorBoundary render error',
1113
+ 'Normal componentWillUnmount',
1114
'ErrorBoundary componentDidUpdate',
1115
]);
1116
@@ -1175,14 +1147,13 @@ describe('ReactErrorBoundaries', () => {
1147
'Normal render',
1148
// BrokenComponentWillReceiveProps will abort rendering:
1149
'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
1178
- // Finish updating with null children
1179
- 'Normal componentWillUnmount',
1180
- 'BrokenComponentWillReceiveProps componentWillUnmount',
1181
- 'ErrorBoundary componentDidUpdate',
1150
// Handle the error
1183
- 'ErrorBoundary componentDidCatch',
1151
+ 'ErrorBoundary static getDerivedStateFromError',
1152
+ // Render the error message
1153
'ErrorBoundary componentWillUpdate',
1154
'ErrorBoundary render error',
1155
+ 'Normal componentWillUnmount',
1156
+ 'BrokenComponentWillReceiveProps componentWillUnmount',
1157
'ErrorBoundary componentDidUpdate',
1158
]);
1159
@@ -1220,14 +1191,12 @@ describe('ReactErrorBoundaries', () => {
1191
// BrokenComponentWillUpdate will abort rendering:
1192
'BrokenComponentWillUpdate componentWillReceiveProps',
1193
'BrokenComponentWillUpdate componentWillUpdate [!]',
1223
- // Finish updating with null children
1224
- 'Normal componentWillUnmount',
1225
- 'BrokenComponentWillUpdate componentWillUnmount',
1226
- 'ErrorBoundary componentDidUpdate',
1194
// Handle the error
1228
- 'ErrorBoundary componentDidCatch',
1195
+ 'ErrorBoundary static getDerivedStateFromError',
1196
'ErrorBoundary componentWillUpdate',
1197
'ErrorBoundary render error',
1198
+ 'Normal componentWillUnmount',
1199
+ 'BrokenComponentWillUpdate componentWillUnmount',
1200
'ErrorBoundary componentDidUpdate',
1201
]);
1202
@@ -1270,13 +1239,11 @@ describe('ReactErrorBoundaries', () => {
1239
'BrokenRender constructor',
1240
'BrokenRender componentWillMount',
1241
'BrokenRender render [!]',
1273
- // Finish updating with null children
1274
- 'Normal componentWillUnmount',
1275
- 'ErrorBoundary componentDidUpdate',
1242
// Handle the error
1277
- 'ErrorBoundary componentDidCatch',
1243
+ 'ErrorBoundary static getDerivedStateFromError',
1244
'ErrorBoundary componentWillUpdate',
1245
'ErrorBoundary render error',
1246
+ 'Normal componentWillUnmount',
1247
'ErrorBoundary componentDidUpdate',
1248
]);
1249
@@ -1329,15 +1296,14 @@ describe('ReactErrorBoundaries', () => {
1296
'BrokenRender constructor',
1297
'BrokenRender componentWillMount',
1298
'BrokenRender render [!]',
1332
- // Finish updating with null children
1333
- 'Child1 ref is set to null',
1334
- 'ErrorBoundary componentDidUpdate',
1299
// Handle the error
1336
- 'ErrorBoundary componentDidCatch',
1300
+ 'ErrorBoundary static getDerivedStateFromError',
1301
'ErrorBoundary componentWillUpdate',
1302
'ErrorBoundary render error',
1339
- 'Error message ref is set to [object HTMLDivElement]',
1303
+ // Update Child1 ref since Child1 has been unmounted
1304
// Child2 ref is never set because its mounting aborted
1305
+ 'Child1 ref is set to null',
1306
+ 'Error message ref is set to [object HTMLDivElement]',
1307
'ErrorBoundary componentDidUpdate',
1308
]);
1309
@@ -1383,15 +1349,15 @@ describe('ReactErrorBoundaries', () => {
1349
// The components have updated in this phase
1350
'BrokenComponentWillUnmount componentDidUpdate',
1351
'ErrorBoundary componentDidUpdate',
1386
- // Now that commit phase is done, Fiber unmounts the boundary's children
1387
- 'BrokenComponentWillUnmount componentWillUnmount [!]',
1388
- 'ErrorBoundary componentDidCatch',
1352
// The initial render was aborted, so
1353
// Fiber retries from the root.
1354
+ 'ErrorBoundary static getDerivedStateFromError',
1355
'ErrorBoundary componentWillUpdate',
1356
+ 'ErrorBoundary render error',
1357
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
1358
'ErrorBoundary componentDidUpdate',
1359
// The second willUnmount error should be captured and logged, too.
1394
- 'ErrorBoundary componentDidCatch',
1360
+ 'ErrorBoundary static getDerivedStateFromError',
1361
'ErrorBoundary componentWillUpdate',
1362
// Render an error now (stack will do it later)
1363
'ErrorBoundary render error',
@@ -1444,16 +1410,15 @@ describe('ReactErrorBoundaries', () => {
1410
'BrokenComponentWillUnmount componentDidUpdate',
1411
'Normal componentDidUpdate',
1412
'ErrorBoundary componentDidUpdate',
1447
- 'Normal componentWillUnmount',
1448
- 'BrokenComponentWillUnmount componentWillUnmount [!]',
1413
// Now that commit phase is done, Fiber handles errors
1450
- 'ErrorBoundary componentDidCatch',
1451
- // The initial render was aborted, so
1452
- // Fiber retries from the root.
1414
+ 'ErrorBoundary static getDerivedStateFromError',
1415
'ErrorBoundary componentWillUpdate',
1416
+ 'ErrorBoundary render error',
1417
+ 'Normal componentWillUnmount',
1418
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
1419
'ErrorBoundary componentDidUpdate',
1420
// The second willUnmount error should be captured and logged, too.
1456
- 'ErrorBoundary componentDidCatch',
1421
+ 'ErrorBoundary static getDerivedStateFromError',
1422
'ErrorBoundary componentWillUpdate',
1423
// Render an error now (stack will do it later)
1424
'ErrorBoundary render error',
@@ -1512,13 +1477,11 @@ describe('ReactErrorBoundaries', () => {
1477
'InnerErrorBoundary render success',
1478
// Try unmounting child
1479
'BrokenComponentWillUnmount componentWillUnmount [!]',
1515
- // Fiber proceeds with lifecycles despite errors
1516
- // Inner and outer boundaries have updated in this phase
1517
- 'InnerErrorBoundary componentDidUpdate',
1518
- 'OuterErrorBoundary componentDidUpdate',
1480
// Now that commit phase is done, Fiber handles errors
1481
// Only inner boundary receives the error:
1521
- 'InnerErrorBoundary componentDidCatch',
1482
+ 'InnerErrorBoundary componentDidUpdate',
1483
+ 'OuterErrorBoundary componentDidUpdate',
1484
+ 'ErrorBoundary static getDerivedStateFromError',
1485
'InnerErrorBoundary componentWillUpdate',
1486
// Render an error now
1487
'InnerErrorBoundary render error',
@@ -1723,7 +1686,7 @@ describe('ReactErrorBoundaries', () => {
1686
1687
expect(log).toEqual([
1688
'Stateful render [!]',
1726
- 'ErrorBoundary componentDidCatch',
1689
+ 'ErrorBoundary static getDerivedStateFromError',
1690
'ErrorBoundary componentWillUpdate',
1691
'ErrorBoundary render error',
1692
'ErrorBoundary componentDidUpdate',
@@ -1768,20 +1731,20 @@ describe('ReactErrorBoundaries', () => {
1731
'BrokenComponentDidMount componentDidMount [!]',
1732
// Continue despite the error
1733
'LastChild componentDidMount',
1771
- 'ErrorBoundary componentDidMount',
1734
// Now we are ready to handle the error
1735
+ 'ErrorBoundary componentDidMount',
1736
+ 'ErrorBoundary static getDerivedStateFromError',
1737
+ 'ErrorBoundary componentWillUpdate',
1738
+ 'ErrorBoundary render error',
1739
// Safely unmount every child
1740
'BrokenComponentWillUnmount componentWillUnmount [!]',
1741
// Continue unmounting safely despite any errors
1742
'Normal componentWillUnmount',
1743
'BrokenComponentDidMount componentWillUnmount',
1744
'LastChild componentWillUnmount',
1779
- // Handle the error
1780
- 'ErrorBoundary componentDidCatch',
1781
- 'ErrorBoundary componentWillUpdate',
1745
// The willUnmount error should be captured and logged, too.
1746
'ErrorBoundary componentDidUpdate',
1784
- 'ErrorBoundary componentDidCatch',
1747
+ 'ErrorBoundary static getDerivedStateFromError',
1748
'ErrorBoundary componentWillUpdate',
1749
'ErrorBoundary render error',
1750
// The update has finished
@@ -1819,11 +1782,11 @@ describe('ReactErrorBoundaries', () => {
1782
// All lifecycles run
1783
'BrokenComponentDidUpdate componentDidUpdate [!]',
1784
'ErrorBoundary componentDidUpdate',
1822
- 'BrokenComponentDidUpdate componentWillUnmount',
1785
// Then, error is handled
1824
- 'ErrorBoundary componentDidCatch',
1786
+ 'ErrorBoundary static getDerivedStateFromError',
1787
'ErrorBoundary componentWillUpdate',
1788
'ErrorBoundary render error',
1789
+ 'BrokenComponentDidUpdate componentWillUnmount',
1790
'ErrorBoundary componentDidUpdate',
1791
]);
1792
@@ -1855,12 +1818,12 @@ describe('ReactErrorBoundaries', () => {
1818
'BrokenComponentDidMountErrorBoundary componentDidMount [!]',
1819
// Fiber proceeds with the hooks
1820
'ErrorBoundary componentDidMount',
1858
- 'BrokenComponentDidMountErrorBoundary componentWillUnmount',
1821
// The error propagates to the higher boundary
1860
- 'ErrorBoundary componentDidCatch',
1822
+ 'ErrorBoundary static getDerivedStateFromError',
1823
// Fiber retries from the root
1824
'ErrorBoundary componentWillUpdate',
1825
'ErrorBoundary render error',
1826
+ 'BrokenComponentDidMountErrorBoundary componentWillUnmount',
1827
'ErrorBoundary componentDidUpdate',
1828
]);
1829
@@ -1869,7 +1832,7 @@ describe('ReactErrorBoundaries', () => {
1832
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1833
});
1834
1872
- it('calls componentDidCatch for each error that is captured', () => {
1835
+ it('calls static getDerivedStateFromError for each error that is captured', () => {
1836
function renderUnmountError(error) {
1837
return <div>Caught an unmounting error: {error.message}.</div>;
1838
}
@@ -1947,16 +1910,16 @@ describe('ReactErrorBoundaries', () => {
1910
'OuterErrorBoundary componentDidUpdate',
1911
// After the commit phase, attempt to recover from any errors that
1912
// were captured
1950
- 'BrokenComponentDidUpdate componentWillUnmount',
1951
- 'BrokenComponentDidUpdate componentWillUnmount',
1952
- 'InnerUnmountBoundary componentDidCatch',
1953
- 'InnerUnmountBoundary componentDidCatch',
1954
- 'InnerUpdateBoundary componentDidCatch',
1955
- 'InnerUpdateBoundary componentDidCatch',
1913
+ 'ErrorBoundary static getDerivedStateFromError',
1914
+ 'ErrorBoundary static getDerivedStateFromError',
1915
'InnerUnmountBoundary componentWillUpdate',
1916
'InnerUnmountBoundary render error',
1917
+ 'ErrorBoundary static getDerivedStateFromError',
1918
+ 'ErrorBoundary static getDerivedStateFromError',
1919
'InnerUpdateBoundary componentWillUpdate',
1920
'InnerUpdateBoundary render error',
1921
+ 'BrokenComponentDidUpdate componentWillUnmount',
1922
+ 'BrokenComponentDidUpdate componentWillUnmount',
1923
'InnerUnmountBoundary componentDidUpdate',
1924
'InnerUpdateBoundary componentDidUpdate',
1925
]);
@@ -2003,16 +1966,18 @@ describe('ReactErrorBoundaries', () => {
1966
1967
it('renders empty output if error boundary does not handle the error', () => {
1968
const container = document.createElement('div');
2006
- ReactDOM.render(
2007
- <div>
2008
- Sibling
2009
- <NoopErrorBoundary>
2010
- <BrokenRender />
2011
- </NoopErrorBoundary>
2012
- </div>,
2013
- container,
2014
- );
2015
- expect(container.firstChild.textContent).toBe('Sibling');
1969
+ expect(() =>
1970
+ ReactDOM.render(
1971
+ <div>
1972
+ Sibling
1973
+ <NoopErrorBoundary>
1974
+ <BrokenRender />
1975
+ </NoopErrorBoundary>
1976
+ </div>,
1977
+ container,
1978
+ ),
1979
+ ).toThrow('Hello');
1980
+ expect(container.innerHTML).toBe('');
1981
expect(log).toEqual([
1982
'NoopErrorBoundary constructor',
1983
'NoopErrorBoundary componentWillMount',
@@ -2020,15 +1985,13 @@ describe('ReactErrorBoundaries', () => {
1985
'BrokenRender constructor',
1986
'BrokenRender componentWillMount',
1987
'BrokenRender render [!]',
2023
- // In Fiber, noop error boundaries render null
2024
- 'NoopErrorBoundary componentDidMount',
2025
- 'NoopErrorBoundary componentDidCatch',
2026
- // Nothing happens.
1988
+ // Noop error boundaries retry render (and fail again)
1989
+ 'NoopErrorBoundary static getDerivedStateFromError',
1990
+ 'NoopErrorBoundary render',
1991
+ 'BrokenRender constructor',
1992
+ 'BrokenRender componentWillMount',
1993
+ 'BrokenRender render [!]',
1994
]);
2028
-
2029
- log.length = 0;
2030
- ReactDOM.unmountComponentAtNode(container);
2031
- expect(log).toEqual(['NoopErrorBoundary componentWillUnmount']);
1995
});
1996
1997
it('passes first error when two errors happen in commit', () => {
@@ -2121,4 +2084,69 @@ describe('ReactErrorBoundaries', () => {
2084
// Error should be the first thrown
2085
expect(caughtError.message).toBe('child sad');
2086
});
2087
+
2088
+ it('should warn if an error boundary with only componentDidCatch does not update state', () => {
2089
+ class InvalidErrorBoundary extends React.Component {
2090
+ componentDidCatch(error, info) {
2091
+ // This component does not define getDerivedStateFromError().
2092
+ // It also doesn't call setState().
2093
+ // So it would swallow errors (which is probably unintentional).
2094
+ }
2095
+ render() {
2096
+ return this.props.children;
2097
+ }
2098
+ }
2099
+
2100
+ const Throws = () => {
2101
+ throw new Error('expected');
2102
+ };
2103
+
2104
+ const container = document.createElement('div');
2105
+ expect(() => {
2106
+ ReactDOM.render(
2107
+ <InvalidErrorBoundary>
2108
+ <Throws />
2109
+ </InvalidErrorBoundary>,
2110
+ container,
2111
+ );
2112
+ }).toWarnDev(
2113
+ 'InvalidErrorBoundary: Error boundaries should implement getDerivedStateFromError(). ' +
2114
+ 'In that method, return a state update to display an error message or fallback UI.',
2115
+ {withoutStack: true},
2116
+ );
2117
+ expect(container.textContent).toBe('');
2118
+ });
2119
+
2120
+ it('should call both componentDidCatch and getDerivedStateFromError if both exist on a component', () => {
2121
+ let componentDidCatchError, getDerivedStateFromErrorError;
2122
+ class ErrorBoundaryWithBothMethods extends React.Component {
2123
+ state = {error: null};
2124
+ static getDerivedStateFromError(error) {
2125
+ getDerivedStateFromErrorError = error;
2126
+ return {error};
2127
+ }
2128
+ componentDidCatch(error, info) {
2129
+ componentDidCatchError = error;
2130
+ }
2131
+ render() {
2132
+ return this.state.error ? 'ErrorBoundary' : this.props.children;
2133
+ }
2134
+ }
2135
+
2136
+ const thrownError = new Error('expected');
2137
+ const Throws = () => {
2138
+ throw thrownError;
2139
+ };
2140
+
2141
+ const container = document.createElement('div');
2142
+ ReactDOM.render(
2143
+ <ErrorBoundaryWithBothMethods>
2144
+ <Throws />
2145
+ </ErrorBoundaryWithBothMethods>,
2146
+ container,
2147
+ );
2148
+ expect(container.textContent).toBe('ErrorBoundary');
2149
+ expect(componentDidCatchError).toBe(thrownError);
2150
+ expect(getDerivedStateFromErrorError).toBe(thrownError);
2151
+ });
2152
});
packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js
new
+2130
@@ -0,0 +1,2130 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+let PropTypes;
13
+let React;
14
+let ReactDOM;
15
+let ReactFeatureFlags;
16
+
17
+// TODO: Refactor this test once componentDidCatch setState is deprecated.
18
+describe('ReactLegacyErrorBoundaries', () => {
19
+ let log;
20
+
21
+ let BrokenConstructor;
22
+ let BrokenComponentWillMount;
23
+ let BrokenComponentDidMount;
24
+ let BrokenComponentWillReceiveProps;
25
+ let BrokenComponentWillUpdate;
26
+ let BrokenComponentDidUpdate;
27
+ let BrokenComponentWillUnmount;
28
+ let BrokenRenderErrorBoundary;
29
+ let BrokenComponentWillMountErrorBoundary;
30
+ let BrokenComponentDidMountErrorBoundary;
31
+ let BrokenRender;
32
+ let ErrorBoundary;
33
+ let ErrorMessage;
34
+ let NoopErrorBoundary;
35
+ let RetryErrorBoundary;
36
+ let Normal;
37
+
38
+ beforeEach(() => {
39
+ jest.resetModules();
40
+ PropTypes = require('prop-types');
41
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
42
+ ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
43
+ ReactDOM = require('react-dom');
44
+ React = require('react');
45
+
46
+ log = [];
47
+
48
+ BrokenConstructor = class extends React.Component {
49
+ constructor(props) {
50
+ super(props);
51
+ log.push('BrokenConstructor constructor [!]');
52
+ throw new Error('Hello');
53
+ }
54
+ render() {
55
+ log.push('BrokenConstructor render');
56
+ return <div>{this.props.children}</div>;
57
+ }
58
+ UNSAFE_componentWillMount() {
59
+ log.push('BrokenConstructor componentWillMount');
60
+ }
61
+ componentDidMount() {
62
+ log.push('BrokenConstructor componentDidMount');
63
+ }
64
+ UNSAFE_componentWillReceiveProps() {
65
+ log.push('BrokenConstructor componentWillReceiveProps');
66
+ }
67
+ UNSAFE_componentWillUpdate() {
68
+ log.push('BrokenConstructor componentWillUpdate');
69
+ }
70
+ componentDidUpdate() {
71
+ log.push('BrokenConstructor componentDidUpdate');
72
+ }
73
+ componentWillUnmount() {
74
+ log.push('BrokenConstructor componentWillUnmount');
75
+ }
76
+ };
77
+
78
+ BrokenComponentWillMount = class extends React.Component {
79
+ constructor(props) {
80
+ super(props);
81
+ log.push('BrokenComponentWillMount constructor');
82
+ }
83
+ render() {
84
+ log.push('BrokenComponentWillMount render');
85
+ return <div>{this.props.children}</div>;
86
+ }
87
+ UNSAFE_componentWillMount() {
88
+ log.push('BrokenComponentWillMount componentWillMount [!]');
89
+ throw new Error('Hello');
90
+ }
91
+ componentDidMount() {
92
+ log.push('BrokenComponentWillMount componentDidMount');
93
+ }
94
+ UNSAFE_componentWillReceiveProps() {
95
+ log.push('BrokenComponentWillMount componentWillReceiveProps');
96
+ }
97
+ UNSAFE_componentWillUpdate() {
98
+ log.push('BrokenComponentWillMount componentWillUpdate');
99
+ }
100
+ componentDidUpdate() {
101
+ log.push('BrokenComponentWillMount componentDidUpdate');
102
+ }
103
+ componentWillUnmount() {
104
+ log.push('BrokenComponentWillMount componentWillUnmount');
105
+ }
106
+ };
107
+
108
+ BrokenComponentDidMount = class extends React.Component {
109
+ constructor(props) {
110
+ super(props);
111
+ log.push('BrokenComponentDidMount constructor');
112
+ }
113
+ render() {
114
+ log.push('BrokenComponentDidMount render');
115
+ return <div>{this.props.children}</div>;
116
+ }
117
+ UNSAFE_componentWillMount() {
118
+ log.push('BrokenComponentDidMount componentWillMount');
119
+ }
120
+ componentDidMount() {
121
+ log.push('BrokenComponentDidMount componentDidMount [!]');
122
+ throw new Error('Hello');
123
+ }
124
+ UNSAFE_componentWillReceiveProps() {
125
+ log.push('BrokenComponentDidMount componentWillReceiveProps');
126
+ }
127
+ UNSAFE_componentWillUpdate() {
128
+ log.push('BrokenComponentDidMount componentWillUpdate');
129
+ }
130
+ componentDidUpdate() {
131
+ log.push('BrokenComponentDidMount componentDidUpdate');
132
+ }
133
+ componentWillUnmount() {
134
+ log.push('BrokenComponentDidMount componentWillUnmount');
135
+ }
136
+ };
137
+
138
+ BrokenComponentWillReceiveProps = class extends React.Component {
139
+ constructor(props) {
140
+ super(props);
141
+ log.push('BrokenComponentWillReceiveProps constructor');
142
+ }
143
+ render() {
144
+ log.push('BrokenComponentWillReceiveProps render');
145
+ return <div>{this.props.children}</div>;
146
+ }
147
+ UNSAFE_componentWillMount() {
148
+ log.push('BrokenComponentWillReceiveProps componentWillMount');
149
+ }
150
+ componentDidMount() {
151
+ log.push('BrokenComponentWillReceiveProps componentDidMount');
152
+ }
153
+ UNSAFE_componentWillReceiveProps() {
154
+ log.push(
155
+ 'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
156
+ );
157
+ throw new Error('Hello');
158
+ }
159
+ UNSAFE_componentWillUpdate() {
160
+ log.push('BrokenComponentWillReceiveProps componentWillUpdate');
161
+ }
162
+ componentDidUpdate() {
163
+ log.push('BrokenComponentWillReceiveProps componentDidUpdate');
164
+ }
165
+ componentWillUnmount() {
166
+ log.push('BrokenComponentWillReceiveProps componentWillUnmount');
167
+ }
168
+ };
169
+
170
+ BrokenComponentWillUpdate = class extends React.Component {
171
+ constructor(props) {
172
+ super(props);
173
+ log.push('BrokenComponentWillUpdate constructor');
174
+ }
175
+ render() {
176
+ log.push('BrokenComponentWillUpdate render');
177
+ return <div>{this.props.children}</div>;
178
+ }
179
+ UNSAFE_componentWillMount() {
180
+ log.push('BrokenComponentWillUpdate componentWillMount');
181
+ }
182
+ componentDidMount() {
183
+ log.push('BrokenComponentWillUpdate componentDidMount');
184
+ }
185
+ UNSAFE_componentWillReceiveProps() {
186
+ log.push('BrokenComponentWillUpdate componentWillReceiveProps');
187
+ }
188
+ UNSAFE_componentWillUpdate() {
189
+ log.push('BrokenComponentWillUpdate componentWillUpdate [!]');
190
+ throw new Error('Hello');
191
+ }
192
+ componentDidUpdate() {
193
+ log.push('BrokenComponentWillUpdate componentDidUpdate');
194
+ }
195
+ componentWillUnmount() {
196
+ log.push('BrokenComponentWillUpdate componentWillUnmount');
197
+ }
198
+ };
199
+
200
+ BrokenComponentDidUpdate = class extends React.Component {
201
+ static defaultProps = {
202
+ errorText: 'Hello',
203
+ };
204
+ constructor(props) {
205
+ super(props);
206
+ log.push('BrokenComponentDidUpdate constructor');
207
+ }
208
+ render() {
209
+ log.push('BrokenComponentDidUpdate render');
210
+ return <div>{this.props.children}</div>;
211
+ }
212
+ UNSAFE_componentWillMount() {
213
+ log.push('BrokenComponentDidUpdate componentWillMount');
214
+ }
215
+ componentDidMount() {
216
+ log.push('BrokenComponentDidUpdate componentDidMount');
217
+ }
218
+ UNSAFE_componentWillReceiveProps() {
219
+ log.push('BrokenComponentDidUpdate componentWillReceiveProps');
220
+ }
221
+ UNSAFE_componentWillUpdate() {
222
+ log.push('BrokenComponentDidUpdate componentWillUpdate');
223
+ }
224
+ componentDidUpdate() {
225
+ log.push('BrokenComponentDidUpdate componentDidUpdate [!]');
226
+ throw new Error(this.props.errorText);
227
+ }
228
+ componentWillUnmount() {
229
+ log.push('BrokenComponentDidUpdate componentWillUnmount');
230
+ }
231
+ };
232
+
233
+ BrokenComponentWillUnmount = class extends React.Component {
234
+ static defaultProps = {
235
+ errorText: 'Hello',
236
+ };
237
+ constructor(props) {
238
+ super(props);
239
+ log.push('BrokenComponentWillUnmount constructor');
240
+ }
241
+ render() {
242
+ log.push('BrokenComponentWillUnmount render');
243
+ return <div>{this.props.children}</div>;
244
+ }
245
+ UNSAFE_componentWillMount() {
246
+ log.push('BrokenComponentWillUnmount componentWillMount');
247
+ }
248
+ componentDidMount() {
249
+ log.push('BrokenComponentWillUnmount componentDidMount');
250
+ }
251
+ UNSAFE_componentWillReceiveProps() {
252
+ log.push('BrokenComponentWillUnmount componentWillReceiveProps');
253
+ }
254
+ UNSAFE_componentWillUpdate() {
255
+ log.push('BrokenComponentWillUnmount componentWillUpdate');
256
+ }
257
+ componentDidUpdate() {
258
+ log.push('BrokenComponentWillUnmount componentDidUpdate');
259
+ }
260
+ componentWillUnmount() {
261
+ log.push('BrokenComponentWillUnmount componentWillUnmount [!]');
262
+ throw new Error(this.props.errorText);
263
+ }
264
+ };
265
+
266
+ BrokenComponentWillMountErrorBoundary = class extends React.Component {
267
+ constructor(props) {
268
+ super(props);
269
+ this.state = {error: null};
270
+ log.push('BrokenComponentWillMountErrorBoundary constructor');
271
+ }
272
+ render() {
273
+ if (this.state.error) {
274
+ log.push('BrokenComponentWillMountErrorBoundary render error');
275
+ return <div>Caught an error: {this.state.error.message}.</div>;
276
+ }
277
+ log.push('BrokenComponentWillMountErrorBoundary render success');
278
+ return <div>{this.props.children}</div>;
279
+ }
280
+ UNSAFE_componentWillMount() {
281
+ log.push(
282
+ 'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
283
+ );
284
+ throw new Error('Hello');
285
+ }
286
+ componentDidMount() {
287
+ log.push('BrokenComponentWillMountErrorBoundary componentDidMount');
288
+ }
289
+ componentWillUnmount() {
290
+ log.push('BrokenComponentWillMountErrorBoundary componentWillUnmount');
291
+ }
292
+ componentDidCatch(error) {
293
+ log.push('BrokenComponentWillMountErrorBoundary componentDidCatch');
294
+ this.setState({error});
295
+ }
296
+ };
297
+
298
+ BrokenComponentDidMountErrorBoundary = class extends React.Component {
299
+ constructor(props) {
300
+ super(props);
301
+ this.state = {error: null};
302
+ log.push('BrokenComponentDidMountErrorBoundary constructor');
303
+ }
304
+ render() {
305
+ if (this.state.error) {
306
+ log.push('BrokenComponentDidMountErrorBoundary render error');
307
+ return <div>Caught an error: {this.state.error.message}.</div>;
308
+ }
309
+ log.push('BrokenComponentDidMountErrorBoundary render success');
310
+ return <div>{this.props.children}</div>;
311
+ }
312
+ UNSAFE_componentWillMount() {
313
+ log.push('BrokenComponentDidMountErrorBoundary componentWillMount');
314
+ }
315
+ componentDidMount() {
316
+ log.push('BrokenComponentDidMountErrorBoundary componentDidMount [!]');
317
+ throw new Error('Hello');
318
+ }
319
+ componentWillUnmount() {
320
+ log.push('BrokenComponentDidMountErrorBoundary componentWillUnmount');
321
+ }
322
+ componentDidCatch(error) {
323
+ log.push('BrokenComponentDidMountErrorBoundary componentDidCatch');
324
+ this.setState({error});
325
+ }
326
+ };
327
+
328
+ BrokenRenderErrorBoundary = class extends React.Component {
329
+ constructor(props) {
330
+ super(props);
331
+ this.state = {error: null};
332
+ log.push('BrokenRenderErrorBoundary constructor');
333
+ }
334
+ render() {
335
+ if (this.state.error) {
336
+ log.push('BrokenRenderErrorBoundary render error [!]');
337
+ throw new Error('Hello');
338
+ }
339
+ log.push('BrokenRenderErrorBoundary render success');
340
+ return <div>{this.props.children}</div>;
341
+ }
342
+ UNSAFE_componentWillMount() {
343
+ log.push('BrokenRenderErrorBoundary componentWillMount');
344
+ }
345
+ componentDidMount() {
346
+ log.push('BrokenRenderErrorBoundary componentDidMount');
347
+ }
348
+ componentWillUnmount() {
349
+ log.push('BrokenRenderErrorBoundary componentWillUnmount');
350
+ }
351
+ componentDidCatch(error) {
352
+ log.push('BrokenRenderErrorBoundary componentDidCatch');
353
+ this.setState({error});
354
+ }
355
+ };
356
+
357
+ BrokenRender = class extends React.Component {
358
+ constructor(props) {
359
+ super(props);
360
+ log.push('BrokenRender constructor');
361
+ }
362
+ render() {
363
+ log.push('BrokenRender render [!]');
364
+ throw new Error('Hello');
365
+ }
366
+ UNSAFE_componentWillMount() {
367
+ log.push('BrokenRender componentWillMount');
368
+ }
369
+ componentDidMount() {
370
+ log.push('BrokenRender componentDidMount');
371
+ }
372
+ UNSAFE_componentWillReceiveProps() {
373
+ log.push('BrokenRender componentWillReceiveProps');
374
+ }
375
+ UNSAFE_componentWillUpdate() {
376
+ log.push('BrokenRender componentWillUpdate');
377
+ }
378
+ componentDidUpdate() {
379
+ log.push('BrokenRender componentDidUpdate');
380
+ }
381
+ componentWillUnmount() {
382
+ log.push('BrokenRender componentWillUnmount');
383
+ }
384
+ };
385
+
386
+ NoopErrorBoundary = class extends React.Component {
387
+ constructor(props) {
388
+ super(props);
389
+ log.push('NoopErrorBoundary constructor');
390
+ }
391
+ render() {
392
+ log.push('NoopErrorBoundary render');
393
+ return <BrokenRender />;
394
+ }
395
+ UNSAFE_componentWillMount() {
396
+ log.push('NoopErrorBoundary componentWillMount');
397
+ }
398
+ componentDidMount() {
399
+ log.push('NoopErrorBoundary componentDidMount');
400
+ }
401
+ componentWillUnmount() {
402
+ log.push('NoopErrorBoundary componentWillUnmount');
403
+ }
404
+ componentDidCatch() {
405
+ log.push('NoopErrorBoundary componentDidCatch');
406
+ }
407
+ };
408
+
409
+ Normal = class extends React.Component {
410
+ static defaultProps = {
411
+ logName: 'Normal',
412
+ };
413
+ constructor(props) {
414
+ super(props);
415
+ log.push(`${this.props.logName} constructor`);
416
+ }
417
+ render() {
418
+ log.push(`${this.props.logName} render`);
419
+ return <div>{this.props.children}</div>;
420
+ }
421
+ UNSAFE_componentWillMount() {
422
+ log.push(`${this.props.logName} componentWillMount`);
423
+ }
424
+ componentDidMount() {
425
+ log.push(`${this.props.logName} componentDidMount`);
426
+ }
427
+ UNSAFE_componentWillReceiveProps() {
428
+ log.push(`${this.props.logName} componentWillReceiveProps`);
429
+ }
430
+ UNSAFE_componentWillUpdate() {
431
+ log.push(`${this.props.logName} componentWillUpdate`);
432
+ }
433
+ componentDidUpdate() {
434
+ log.push(`${this.props.logName} componentDidUpdate`);
435
+ }
436
+ componentWillUnmount() {
437
+ log.push(`${this.props.logName} componentWillUnmount`);
438
+ }
439
+ };
440
+
441
+ ErrorBoundary = class extends React.Component {
442
+ constructor(props) {
443
+ super(props);
444
+ this.state = {error: null};
445
+ log.push(`${this.props.logName} constructor`);
446
+ }
447
+ render() {
448
+ if (this.state.error && !this.props.forceRetry) {
449
+ log.push(`${this.props.logName} render error`);
450
+ return this.props.renderError(this.state.error, this.props);
451
+ }
452
+ log.push(`${this.props.logName} render success`);
453
+ return <div>{this.props.children}</div>;
454
+ }
455
+ componentDidCatch(error) {
456
+ log.push(`${this.props.logName} componentDidCatch`);
457
+ this.setState({error});
458
+ }
459
+ UNSAFE_componentWillMount() {
460
+ log.push(`${this.props.logName} componentWillMount`);
461
+ }
462
+ componentDidMount() {
463
+ log.push(`${this.props.logName} componentDidMount`);
464
+ }
465
+ UNSAFE_componentWillReceiveProps() {
466
+ log.push(`${this.props.logName} componentWillReceiveProps`);
467
+ }
468
+ UNSAFE_componentWillUpdate() {
469
+ log.push(`${this.props.logName} componentWillUpdate`);
470
+ }
471
+ componentDidUpdate() {
472
+ log.push(`${this.props.logName} componentDidUpdate`);
473
+ }
474
+ componentWillUnmount() {
475
+ log.push(`${this.props.logName} componentWillUnmount`);
476
+ }
477
+ };
478
+ ErrorBoundary.defaultProps = {
479
+ logName: 'ErrorBoundary',
480
+ renderError(error, props) {
481
+ return (
482
+ <div ref={props.errorMessageRef}>
483
+ Caught an error: {error.message}.
484
+ </div>
485
+ );
486
+ },
487
+ };
488
+
489
+ RetryErrorBoundary = class extends React.Component {
490
+ constructor(props) {
491
+ super(props);
492
+ log.push('RetryErrorBoundary constructor');
493
+ }
494
+ render() {
495
+ log.push('RetryErrorBoundary render');
496
+ return <BrokenRender />;
497
+ }
498
+ UNSAFE_componentWillMount() {
499
+ log.push('RetryErrorBoundary componentWillMount');
500
+ }
501
+ componentDidMount() {
502
+ log.push('RetryErrorBoundary componentDidMount');
503
+ }
504
+ componentWillUnmount() {
505
+ log.push('RetryErrorBoundary componentWillUnmount');
506
+ }
507
+ componentDidCatch(error) {
508
+ log.push('RetryErrorBoundary componentDidCatch [!]');
509
+ // In Fiber, calling setState() (and failing) is treated as a rethrow.
510
+ this.setState({});
511
+ }
512
+ };
513
+
514
+ ErrorMessage = class extends React.Component {
515
+ constructor(props) {
516
+ super(props);
517
+ log.push('ErrorMessage constructor');
518
+ }
519
+ UNSAFE_componentWillMount() {
520
+ log.push('ErrorMessage componentWillMount');
521
+ }
522
+ componentDidMount() {
523
+ log.push('ErrorMessage componentDidMount');
524
+ }
525
+ componentWillUnmount() {
526
+ log.push('ErrorMessage componentWillUnmount');
527
+ }
528
+ render() {
529
+ log.push('ErrorMessage render');
530
+ return <div>Caught an error: {this.props.message}.</div>;
531
+ }
532
+ };
533
+ });
534
+
535
+ it('does not swallow exceptions on mounting without boundaries', () => {
536
+ let container = document.createElement('div');
537
+ expect(() => {
538
+ ReactDOM.render(<BrokenRender />, container);
539
+ }).toThrow('Hello');
540
+
541
+ container = document.createElement('div');
542
+ expect(() => {
543
+ ReactDOM.render(<BrokenComponentWillMount />, container);
544
+ }).toThrow('Hello');
545
+
546
+ container = document.createElement('div');
547
+ expect(() => {
548
+ ReactDOM.render(<BrokenComponentDidMount />, container);
549
+ }).toThrow('Hello');
550
+ });
551
+
552
+ it('does not swallow exceptions on updating without boundaries', () => {
553
+ let container = document.createElement('div');
554
+ ReactDOM.render(<BrokenComponentWillUpdate />, container);
555
+ expect(() => {
556
+ ReactDOM.render(<BrokenComponentWillUpdate />, container);
557
+ }).toThrow('Hello');
558
+
559
+ container = document.createElement('div');
560
+ ReactDOM.render(<BrokenComponentWillReceiveProps />, container);
561
+ expect(() => {
562
+ ReactDOM.render(<BrokenComponentWillReceiveProps />, container);
563
+ }).toThrow('Hello');
564
+
565
+ container = document.createElement('div');
566
+ ReactDOM.render(<BrokenComponentDidUpdate />, container);
567
+ expect(() => {
568
+ ReactDOM.render(<BrokenComponentDidUpdate />, container);
569
+ }).toThrow('Hello');
570
+ });
571
+
572
+ it('does not swallow exceptions on unmounting without boundaries', () => {
573
+ const container = document.createElement('div');
574
+ ReactDOM.render(<BrokenComponentWillUnmount />, container);
575
+ expect(() => {
576
+ ReactDOM.unmountComponentAtNode(container);
577
+ }).toThrow('Hello');
578
+ });
579
+
580
+ it('prevents errors from leaking into other roots', () => {
581
+ const container1 = document.createElement('div');
582
+ const container2 = document.createElement('div');
583
+ const container3 = document.createElement('div');
584
+
585
+ ReactDOM.render(<span>Before 1</span>, container1);
586
+ expect(() => {
587
+ ReactDOM.render(<BrokenRender />, container2);
588
+ }).toThrow('Hello');
589
+ ReactDOM.render(
590
+ <ErrorBoundary>
591
+ <BrokenRender />
592
+ </ErrorBoundary>,
593
+ container3,
594
+ );
595
+ expect(container1.firstChild.textContent).toBe('Before 1');
596
+ expect(container2.firstChild).toBe(null);
597
+ expect(container3.firstChild.textContent).toBe('Caught an error: Hello.');
598
+
599
+ ReactDOM.render(<span>After 1</span>, container1);
600
+ ReactDOM.render(<span>After 2</span>, container2);
601
+ ReactDOM.render(
602
+ <ErrorBoundary forceRetry={true}>After 3</ErrorBoundary>,
603
+ container3,
604
+ );
605
+ expect(container1.firstChild.textContent).toBe('After 1');
606
+ expect(container2.firstChild.textContent).toBe('After 2');
607
+ expect(container3.firstChild.textContent).toBe('After 3');
608
+
609
+ ReactDOM.unmountComponentAtNode(container1);
610
+ ReactDOM.unmountComponentAtNode(container2);
611
+ ReactDOM.unmountComponentAtNode(container3);
612
+ expect(container1.firstChild).toBe(null);
613
+ expect(container2.firstChild).toBe(null);
614
+ expect(container3.firstChild).toBe(null);
615
+ });
616
+
617
+ it('renders an error state if child throws in render', () => {
618
+ const container = document.createElement('div');
619
+ ReactDOM.render(
620
+ <ErrorBoundary>
621
+ <BrokenRender />
622
+ </ErrorBoundary>,
623
+ container,
624
+ );
625
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
626
+ expect(log).toEqual([
627
+ 'ErrorBoundary constructor',
628
+ 'ErrorBoundary componentWillMount',
629
+ 'ErrorBoundary render success',
630
+ 'BrokenRender constructor',
631
+ 'BrokenRender componentWillMount',
632
+ 'BrokenRender render [!]',
633
+ // Fiber mounts with null children before capturing error
634
+ 'ErrorBoundary componentDidMount',
635
+ // Catch and render an error message
636
+ 'ErrorBoundary componentDidCatch',
637
+ 'ErrorBoundary componentWillUpdate',
638
+ 'ErrorBoundary render error',
639
+ 'ErrorBoundary componentDidUpdate',
640
+ ]);
641
+
642
+ log.length = 0;
643
+ ReactDOM.unmountComponentAtNode(container);
644
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
645
+ });
646
+
647
+ it('renders an error state if child throws in constructor', () => {
648
+ const container = document.createElement('div');
649
+ ReactDOM.render(
650
+ <ErrorBoundary>
651
+ <BrokenConstructor />
652
+ </ErrorBoundary>,
653
+ container,
654
+ );
655
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
656
+ expect(log).toEqual([
657
+ 'ErrorBoundary constructor',
658
+ 'ErrorBoundary componentWillMount',
659
+ 'ErrorBoundary render success',
660
+ 'BrokenConstructor constructor [!]',
661
+ // Fiber mounts with null children before capturing error
662
+ 'ErrorBoundary componentDidMount',
663
+ // Catch and render an error message
664
+ 'ErrorBoundary componentDidCatch',
665
+ 'ErrorBoundary componentWillUpdate',
666
+ 'ErrorBoundary render error',
667
+ 'ErrorBoundary componentDidUpdate',
668
+ ]);
669
+
670
+ log.length = 0;
671
+ ReactDOM.unmountComponentAtNode(container);
672
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
673
+ });
674
+
675
+ it('renders an error state if child throws in componentWillMount', () => {
676
+ const container = document.createElement('div');
677
+ ReactDOM.render(
678
+ <ErrorBoundary>
679
+ <BrokenComponentWillMount />
680
+ </ErrorBoundary>,
681
+ container,
682
+ );
683
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
684
+ expect(log).toEqual([
685
+ 'ErrorBoundary constructor',
686
+ 'ErrorBoundary componentWillMount',
687
+ 'ErrorBoundary render success',
688
+ 'BrokenComponentWillMount constructor',
689
+ 'BrokenComponentWillMount componentWillMount [!]',
690
+ 'ErrorBoundary componentDidMount',
691
+ 'ErrorBoundary componentDidCatch',
692
+ 'ErrorBoundary componentWillUpdate',
693
+ 'ErrorBoundary render error',
694
+ 'ErrorBoundary componentDidUpdate',
695
+ ]);
696
+
697
+ log.length = 0;
698
+ ReactDOM.unmountComponentAtNode(container);
699
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
700
+ });
701
+
702
+ it('renders an error state if context provider throws in componentWillMount', () => {
703
+ class BrokenComponentWillMountWithContext extends React.Component {
704
+ static childContextTypes = {foo: PropTypes.number};
705
+ getChildContext() {
706
+ return {foo: 42};
707
+ }
708
+ render() {
709
+ return <div>{this.props.children}</div>;
710
+ }
711
+ UNSAFE_componentWillMount() {
712
+ throw new Error('Hello');
713
+ }
714
+ }
715
+
716
+ const container = document.createElement('div');
717
+ ReactDOM.render(
718
+ <ErrorBoundary>
719
+ <BrokenComponentWillMountWithContext />
720
+ </ErrorBoundary>,
721
+ container,
722
+ );
723
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
724
+ });
725
+
726
+ it('renders an error state if module-style context provider throws in componentWillMount', () => {
727
+ function BrokenComponentWillMountWithContext() {
728
+ return {
729
+ getChildContext() {
730
+ return {foo: 42};
731
+ },
732
+ render() {
733
+ return <div>{this.props.children}</div>;
734
+ },
735
+ UNSAFE_componentWillMount() {
736
+ throw new Error('Hello');
737
+ },
738
+ };
739
+ }
740
+ BrokenComponentWillMountWithContext.childContextTypes = {
741
+ foo: PropTypes.number,
742
+ };
743
+
744
+ const container = document.createElement('div');
745
+ ReactDOM.render(
746
+ <ErrorBoundary>
747
+ <BrokenComponentWillMountWithContext />
748
+ </ErrorBoundary>,
749
+ container,
750
+ );
751
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
752
+ });
753
+
754
+ it('mounts the error message if mounting fails', () => {
755
+ function renderError(error) {
756
+ return <ErrorMessage message={error.message} />;
757
+ }
758
+
759
+ const container = document.createElement('div');
760
+ ReactDOM.render(
761
+ <ErrorBoundary renderError={renderError}>
762
+ <BrokenRender />
763
+ </ErrorBoundary>,
764
+ container,
765
+ );
766
+ expect(log).toEqual([
767
+ 'ErrorBoundary constructor',
768
+ 'ErrorBoundary componentWillMount',
769
+ 'ErrorBoundary render success',
770
+ 'BrokenRender constructor',
771
+ 'BrokenRender componentWillMount',
772
+ 'BrokenRender render [!]',
773
+ 'ErrorBoundary componentDidMount',
774
+ 'ErrorBoundary componentDidCatch',
775
+ 'ErrorBoundary componentWillUpdate',
776
+ 'ErrorBoundary render error',
777
+ 'ErrorMessage constructor',
778
+ 'ErrorMessage componentWillMount',
779
+ 'ErrorMessage render',
780
+ 'ErrorMessage componentDidMount',
781
+ 'ErrorBoundary componentDidUpdate',
782
+ ]);
783
+
784
+ log.length = 0;
785
+ ReactDOM.unmountComponentAtNode(container);
786
+ expect(log).toEqual([
787
+ 'ErrorBoundary componentWillUnmount',
788
+ 'ErrorMessage componentWillUnmount',
789
+ ]);
790
+ });
791
+
792
+ it('propagates errors on retry on mounting', () => {
793
+ const container = document.createElement('div');
794
+ ReactDOM.render(
795
+ <ErrorBoundary>
796
+ <RetryErrorBoundary>
797
+ <BrokenRender />
798
+ </RetryErrorBoundary>
799
+ </ErrorBoundary>,
800
+ container,
801
+ );
802
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
803
+ expect(log).toEqual([
804
+ 'ErrorBoundary constructor',
805
+ 'ErrorBoundary componentWillMount',
806
+ 'ErrorBoundary render success',
807
+ 'RetryErrorBoundary constructor',
808
+ 'RetryErrorBoundary componentWillMount',
809
+ 'RetryErrorBoundary render',
810
+ 'BrokenRender constructor',
811
+ 'BrokenRender componentWillMount',
812
+ 'BrokenRender render [!]',
813
+ // In Fiber, failed error boundaries render null before attempting to recover
814
+ 'RetryErrorBoundary componentDidMount',
815
+ 'RetryErrorBoundary componentDidCatch [!]',
816
+ 'ErrorBoundary componentDidMount',
817
+ // Retry
818
+ 'RetryErrorBoundary render',
819
+ 'BrokenRender constructor',
820
+ 'BrokenRender componentWillMount',
821
+ 'BrokenRender render [!]',
822
+ // This time, the error propagates to the higher boundary
823
+ 'RetryErrorBoundary componentWillUnmount',
824
+ 'ErrorBoundary componentDidCatch',
825
+ // Render the error
826
+ 'ErrorBoundary componentWillUpdate',
827
+ 'ErrorBoundary render error',
828
+ 'ErrorBoundary componentDidUpdate',
829
+ ]);
830
+
831
+ log.length = 0;
832
+ ReactDOM.unmountComponentAtNode(container);
833
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
834
+ });
835
+
836
+ it('propagates errors inside boundary during componentWillMount', () => {
837
+ const container = document.createElement('div');
838
+ ReactDOM.render(
839
+ <ErrorBoundary>
840
+ <BrokenComponentWillMountErrorBoundary />
841
+ </ErrorBoundary>,
842
+ container,
843
+ );
844
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
845
+ expect(log).toEqual([
846
+ 'ErrorBoundary constructor',
847
+ 'ErrorBoundary componentWillMount',
848
+ 'ErrorBoundary render success',
849
+ 'BrokenComponentWillMountErrorBoundary constructor',
850
+ 'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
851
+ // The error propagates to the higher boundary
852
+ 'ErrorBoundary componentDidMount',
853
+ 'ErrorBoundary componentDidCatch',
854
+ 'ErrorBoundary componentWillUpdate',
855
+ 'ErrorBoundary render error',
856
+ 'ErrorBoundary componentDidUpdate',
857
+ ]);
858
+
859
+ log.length = 0;
860
+ ReactDOM.unmountComponentAtNode(container);
861
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
862
+ });
863
+
864
+ it('propagates errors inside boundary while rendering error state', () => {
865
+ const container = document.createElement('div');
866
+ ReactDOM.render(
867
+ <ErrorBoundary>
868
+ <BrokenRenderErrorBoundary>
869
+ <BrokenRender />
870
+ </BrokenRenderErrorBoundary>
871
+ </ErrorBoundary>,
872
+ container,
873
+ );
874
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
875
+ expect(log).toEqual([
876
+ 'ErrorBoundary constructor',
877
+ 'ErrorBoundary componentWillMount',
878
+ 'ErrorBoundary render success',
879
+ 'BrokenRenderErrorBoundary constructor',
880
+ 'BrokenRenderErrorBoundary componentWillMount',
881
+ 'BrokenRenderErrorBoundary render success',
882
+ 'BrokenRender constructor',
883
+ 'BrokenRender componentWillMount',
884
+ 'BrokenRender render [!]',
885
+ // The first error boundary catches the error
886
+ // It adjusts state but throws displaying the message
887
+ // Finish mounting with null children
888
+ 'BrokenRenderErrorBoundary componentDidMount',
889
+ // Attempt to handle the error
890
+ 'BrokenRenderErrorBoundary componentDidCatch',
891
+ 'ErrorBoundary componentDidMount',
892
+ 'BrokenRenderErrorBoundary render error [!]',
893
+ // Boundary fails with new error, propagate to next boundary
894
+ 'BrokenRenderErrorBoundary componentWillUnmount',
895
+ // Attempt to handle the error again
896
+ 'ErrorBoundary componentDidCatch',
897
+ 'ErrorBoundary componentWillUpdate',
898
+ 'ErrorBoundary render error',
899
+ 'ErrorBoundary componentDidUpdate',
900
+ ]);
901
+
902
+ log.length = 0;
903
+ ReactDOM.unmountComponentAtNode(container);
904
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
905
+ });
906
+
907
+ it('does not call componentWillUnmount when aborting initial mount', () => {
908
+ const container = document.createElement('div');
909
+ ReactDOM.render(
910
+ <ErrorBoundary>
911
+ <Normal />
912
+ <BrokenRender />
913
+ <Normal />
914
+ </ErrorBoundary>,
915
+ container,
916
+ );
917
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
918
+ expect(log).toEqual([
919
+ 'ErrorBoundary constructor',
920
+ 'ErrorBoundary componentWillMount',
921
+ 'ErrorBoundary render success',
922
+ // Render first child
923
+ 'Normal constructor',
924
+ 'Normal componentWillMount',
925
+ 'Normal render',
926
+ // Render second child (it throws)
927
+ 'BrokenRender constructor',
928
+ 'BrokenRender componentWillMount',
929
+ 'BrokenRender render [!]',
930
+ // Render third child, even though an earlier sibling threw.
931
+ 'Normal constructor',
932
+ 'Normal componentWillMount',
933
+ 'Normal render',
934
+ // Finish mounting with null children
935
+ 'ErrorBoundary componentDidMount',
936
+ // Handle the error
937
+ 'ErrorBoundary componentDidCatch',
938
+ // Render the error message
939
+ 'ErrorBoundary componentWillUpdate',
940
+ 'ErrorBoundary render error',
941
+ 'ErrorBoundary componentDidUpdate',
942
+ ]);
943
+
944
+ log.length = 0;
945
+ ReactDOM.unmountComponentAtNode(container);
946
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
947
+ });
948
+
949
+ it('resets callback refs if mounting aborts', () => {
950
+ function childRef(x) {
951
+ log.push('Child ref is set to ' + x);
952
+ }
953
+ function errorMessageRef(x) {
954
+ log.push('Error message ref is set to ' + x);
955
+ }
956
+
957
+ const container = document.createElement('div');
958
+ ReactDOM.render(
959
+ <ErrorBoundary errorMessageRef={errorMessageRef}>
960
+ <div ref={childRef} />
961
+ <BrokenRender />
962
+ </ErrorBoundary>,
963
+ container,
964
+ );
965
+ expect(container.textContent).toBe('Caught an error: Hello.');
966
+ expect(log).toEqual([
967
+ 'ErrorBoundary constructor',
968
+ 'ErrorBoundary componentWillMount',
969
+ 'ErrorBoundary render success',
970
+ 'BrokenRender constructor',
971
+ 'BrokenRender componentWillMount',
972
+ 'BrokenRender render [!]',
973
+ // Handle error:
974
+ // Finish mounting with null children
975
+ 'ErrorBoundary componentDidMount',
976
+ // Handle the error
977
+ 'ErrorBoundary componentDidCatch',
978
+ // Render the error message
979
+ 'ErrorBoundary componentWillUpdate',
980
+ 'ErrorBoundary render error',
981
+ 'Error message ref is set to [object HTMLDivElement]',
982
+ 'ErrorBoundary componentDidUpdate',
983
+ ]);
984
+
985
+ log.length = 0;
986
+ ReactDOM.unmountComponentAtNode(container);
987
+ expect(log).toEqual([
988
+ 'ErrorBoundary componentWillUnmount',
989
+ 'Error message ref is set to null',
990
+ ]);
991
+ });
992
+
993
+ it('resets object refs if mounting aborts', () => {
994
+ let childRef = React.createRef();
995
+ let errorMessageRef = React.createRef();
996
+
997
+ const container = document.createElement('div');
998
+ ReactDOM.render(
999
+ <ErrorBoundary errorMessageRef={errorMessageRef}>
1000
+ <div ref={childRef} />
1001
+ <BrokenRender />
1002
+ </ErrorBoundary>,
1003
+ container,
1004
+ );
1005
+ expect(container.textContent).toBe('Caught an error: Hello.');
1006
+ expect(log).toEqual([
1007
+ 'ErrorBoundary constructor',
1008
+ 'ErrorBoundary componentWillMount',
1009
+ 'ErrorBoundary render success',
1010
+ 'BrokenRender constructor',
1011
+ 'BrokenRender componentWillMount',
1012
+ 'BrokenRender render [!]',
1013
+ // Handle error:
1014
+ // Finish mounting with null children
1015
+ 'ErrorBoundary componentDidMount',
1016
+ // Handle the error
1017
+ 'ErrorBoundary componentDidCatch',
1018
+ // Render the error message
1019
+ 'ErrorBoundary componentWillUpdate',
1020
+ 'ErrorBoundary render error',
1021
+ 'ErrorBoundary componentDidUpdate',
1022
+ ]);
1023
+ expect(errorMessageRef.current.toString()).toEqual(
1024
+ '[object HTMLDivElement]',
1025
+ );
1026
+
1027
+ log.length = 0;
1028
+ ReactDOM.unmountComponentAtNode(container);
1029
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1030
+ expect(errorMessageRef.current).toEqual(null);
1031
+ });
1032
+
1033
+ it('successfully mounts if no error occurs', () => {
1034
+ const container = document.createElement('div');
1035
+ ReactDOM.render(
1036
+ <ErrorBoundary>
1037
+ <div>Mounted successfully.</div>
1038
+ </ErrorBoundary>,
1039
+ container,
1040
+ );
1041
+ expect(container.firstChild.textContent).toBe('Mounted successfully.');
1042
+ expect(log).toEqual([
1043
+ 'ErrorBoundary constructor',
1044
+ 'ErrorBoundary componentWillMount',
1045
+ 'ErrorBoundary render success',
1046
+ 'ErrorBoundary componentDidMount',
1047
+ ]);
1048
+
1049
+ log.length = 0;
1050
+ ReactDOM.unmountComponentAtNode(container);
1051
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1052
+ });
1053
+
1054
+ it('catches if child throws in constructor during update', () => {
1055
+ const container = document.createElement('div');
1056
+ ReactDOM.render(
1057
+ <ErrorBoundary>
1058
+ <Normal />
1059
+ </ErrorBoundary>,
1060
+ container,
1061
+ );
1062
+
1063
+ log.length = 0;
1064
+ ReactDOM.render(
1065
+ <ErrorBoundary>
1066
+ <Normal />
1067
+ <Normal logName="Normal2" />
1068
+ <BrokenConstructor />
1069
+ </ErrorBoundary>,
1070
+ container,
1071
+ );
1072
+ expect(container.textContent).toBe('Caught an error: Hello.');
1073
+ expect(log).toEqual([
1074
+ 'ErrorBoundary componentWillReceiveProps',
1075
+ 'ErrorBoundary componentWillUpdate',
1076
+ 'ErrorBoundary render success',
1077
+ 'Normal componentWillReceiveProps',
1078
+ 'Normal componentWillUpdate',
1079
+ 'Normal render',
1080
+ // Normal2 will attempt to mount:
1081
+ 'Normal2 constructor',
1082
+ 'Normal2 componentWillMount',
1083
+ 'Normal2 render',
1084
+ // BrokenConstructor will abort rendering:
1085
+ 'BrokenConstructor constructor [!]',
1086
+ // Finish updating with null children
1087
+ 'Normal componentWillUnmount',
1088
+ 'ErrorBoundary componentDidUpdate',
1089
+ // Handle the error
1090
+ 'ErrorBoundary componentDidCatch',
1091
+ // Render the error message
1092
+ 'ErrorBoundary componentWillUpdate',
1093
+ 'ErrorBoundary render error',
1094
+ 'ErrorBoundary componentDidUpdate',
1095
+ ]);
1096
+
1097
+ log.length = 0;
1098
+ ReactDOM.unmountComponentAtNode(container);
1099
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1100
+ });
1101
+
1102
+ it('catches if child throws in componentWillMount during update', () => {
1103
+ const container = document.createElement('div');
1104
+ ReactDOM.render(
1105
+ <ErrorBoundary>
1106
+ <Normal />
1107
+ </ErrorBoundary>,
1108
+ container,
1109
+ );
1110
+
1111
+ log.length = 0;
1112
+ ReactDOM.render(
1113
+ <ErrorBoundary>
1114
+ <Normal />
1115
+ <Normal logName="Normal2" />
1116
+ <BrokenComponentWillMount />
1117
+ </ErrorBoundary>,
1118
+ container,
1119
+ );
1120
+ expect(container.textContent).toBe('Caught an error: Hello.');
1121
+ expect(log).toEqual([
1122
+ 'ErrorBoundary componentWillReceiveProps',
1123
+ 'ErrorBoundary componentWillUpdate',
1124
+ 'ErrorBoundary render success',
1125
+ 'Normal componentWillReceiveProps',
1126
+ 'Normal componentWillUpdate',
1127
+ 'Normal render',
1128
+ // Normal2 will attempt to mount:
1129
+ 'Normal2 constructor',
1130
+ 'Normal2 componentWillMount',
1131
+ 'Normal2 render',
1132
+ // BrokenComponentWillMount will abort rendering:
1133
+ 'BrokenComponentWillMount constructor',
1134
+ 'BrokenComponentWillMount componentWillMount [!]',
1135
+ // Finish updating with null children
1136
+ 'Normal componentWillUnmount',
1137
+ 'ErrorBoundary componentDidUpdate',
1138
+ // Handle the error
1139
+ 'ErrorBoundary componentDidCatch',
1140
+ // Render the error message
1141
+ 'ErrorBoundary componentWillUpdate',
1142
+ 'ErrorBoundary render error',
1143
+ 'ErrorBoundary componentDidUpdate',
1144
+ ]);
1145
+
1146
+ log.length = 0;
1147
+ ReactDOM.unmountComponentAtNode(container);
1148
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1149
+ });
1150
+
1151
+ it('catches if child throws in componentWillReceiveProps during update', () => {
1152
+ const container = document.createElement('div');
1153
+ ReactDOM.render(
1154
+ <ErrorBoundary>
1155
+ <Normal />
1156
+ <BrokenComponentWillReceiveProps />
1157
+ </ErrorBoundary>,
1158
+ container,
1159
+ );
1160
+
1161
+ log.length = 0;
1162
+ ReactDOM.render(
1163
+ <ErrorBoundary>
1164
+ <Normal />
1165
+ <BrokenComponentWillReceiveProps />
1166
+ </ErrorBoundary>,
1167
+ container,
1168
+ );
1169
+ expect(container.textContent).toBe('Caught an error: Hello.');
1170
+ expect(log).toEqual([
1171
+ 'ErrorBoundary componentWillReceiveProps',
1172
+ 'ErrorBoundary componentWillUpdate',
1173
+ 'ErrorBoundary render success',
1174
+ 'Normal componentWillReceiveProps',
1175
+ 'Normal componentWillUpdate',
1176
+ 'Normal render',
1177
+ // BrokenComponentWillReceiveProps will abort rendering:
1178
+ 'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
1179
+ // Finish updating with null children
1180
+ 'Normal componentWillUnmount',
1181
+ 'BrokenComponentWillReceiveProps componentWillUnmount',
1182
+ 'ErrorBoundary componentDidUpdate',
1183
+ // Handle the error
1184
+ 'ErrorBoundary componentDidCatch',
1185
+ 'ErrorBoundary componentWillUpdate',
1186
+ 'ErrorBoundary render error',
1187
+ 'ErrorBoundary componentDidUpdate',
1188
+ ]);
1189
+
1190
+ log.length = 0;
1191
+ ReactDOM.unmountComponentAtNode(container);
1192
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1193
+ });
1194
+
1195
+ it('catches if child throws in componentWillUpdate during update', () => {
1196
+ const container = document.createElement('div');
1197
+ ReactDOM.render(
1198
+ <ErrorBoundary>
1199
+ <Normal />
1200
+ <BrokenComponentWillUpdate />
1201
+ </ErrorBoundary>,
1202
+ container,
1203
+ );
1204
+
1205
+ log.length = 0;
1206
+ ReactDOM.render(
1207
+ <ErrorBoundary>
1208
+ <Normal />
1209
+ <BrokenComponentWillUpdate />
1210
+ </ErrorBoundary>,
1211
+ container,
1212
+ );
1213
+ expect(container.textContent).toBe('Caught an error: Hello.');
1214
+ expect(log).toEqual([
1215
+ 'ErrorBoundary componentWillReceiveProps',
1216
+ 'ErrorBoundary componentWillUpdate',
1217
+ 'ErrorBoundary render success',
1218
+ 'Normal componentWillReceiveProps',
1219
+ 'Normal componentWillUpdate',
1220
+ 'Normal render',
1221
+ // BrokenComponentWillUpdate will abort rendering:
1222
+ 'BrokenComponentWillUpdate componentWillReceiveProps',
1223
+ 'BrokenComponentWillUpdate componentWillUpdate [!]',
1224
+ // Finish updating with null children
1225
+ 'Normal componentWillUnmount',
1226
+ 'BrokenComponentWillUpdate componentWillUnmount',
1227
+ 'ErrorBoundary componentDidUpdate',
1228
+ // Handle the error
1229
+ 'ErrorBoundary componentDidCatch',
1230
+ 'ErrorBoundary componentWillUpdate',
1231
+ 'ErrorBoundary render error',
1232
+ 'ErrorBoundary componentDidUpdate',
1233
+ ]);
1234
+
1235
+ log.length = 0;
1236
+ ReactDOM.unmountComponentAtNode(container);
1237
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1238
+ });
1239
+
1240
+ it('catches if child throws in render during update', () => {
1241
+ const container = document.createElement('div');
1242
+ ReactDOM.render(
1243
+ <ErrorBoundary>
1244
+ <Normal />
1245
+ </ErrorBoundary>,
1246
+ container,
1247
+ );
1248
+
1249
+ log.length = 0;
1250
+ ReactDOM.render(
1251
+ <ErrorBoundary>
1252
+ <Normal />
1253
+ <Normal logName="Normal2" />
1254
+ <BrokenRender />
1255
+ </ErrorBoundary>,
1256
+ container,
1257
+ );
1258
+ expect(container.textContent).toBe('Caught an error: Hello.');
1259
+ expect(log).toEqual([
1260
+ 'ErrorBoundary componentWillReceiveProps',
1261
+ 'ErrorBoundary componentWillUpdate',
1262
+ 'ErrorBoundary render success',
1263
+ 'Normal componentWillReceiveProps',
1264
+ 'Normal componentWillUpdate',
1265
+ 'Normal render',
1266
+ // Normal2 will attempt to mount:
1267
+ 'Normal2 constructor',
1268
+ 'Normal2 componentWillMount',
1269
+ 'Normal2 render',
1270
+ // BrokenRender will abort rendering:
1271
+ 'BrokenRender constructor',
1272
+ 'BrokenRender componentWillMount',
1273
+ 'BrokenRender render [!]',
1274
+ // Finish updating with null children
1275
+ 'Normal componentWillUnmount',
1276
+ 'ErrorBoundary componentDidUpdate',
1277
+ // Handle the error
1278
+ 'ErrorBoundary componentDidCatch',
1279
+ 'ErrorBoundary componentWillUpdate',
1280
+ 'ErrorBoundary render error',
1281
+ 'ErrorBoundary componentDidUpdate',
1282
+ ]);
1283
+
1284
+ log.length = 0;
1285
+ ReactDOM.unmountComponentAtNode(container);
1286
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1287
+ });
1288
+
1289
+ it('keeps refs up-to-date during updates', () => {
1290
+ function child1Ref(x) {
1291
+ log.push('Child1 ref is set to ' + x);
1292
+ }
1293
+ function child2Ref(x) {
1294
+ log.push('Child2 ref is set to ' + x);
1295
+ }
1296
+ function errorMessageRef(x) {
1297
+ log.push('Error message ref is set to ' + x);
1298
+ }
1299
+
1300
+ const container = document.createElement('div');
1301
+ ReactDOM.render(
1302
+ <ErrorBoundary errorMessageRef={errorMessageRef}>
1303
+ <div ref={child1Ref} />
1304
+ </ErrorBoundary>,
1305
+ container,
1306
+ );
1307
+ expect(log).toEqual([
1308
+ 'ErrorBoundary constructor',
1309
+ 'ErrorBoundary componentWillMount',
1310
+ 'ErrorBoundary render success',
1311
+ 'Child1 ref is set to [object HTMLDivElement]',
1312
+ 'ErrorBoundary componentDidMount',
1313
+ ]);
1314
+
1315
+ log.length = 0;
1316
+ ReactDOM.render(
1317
+ <ErrorBoundary errorMessageRef={errorMessageRef}>
1318
+ <div ref={child1Ref} />
1319
+ <div ref={child2Ref} />
1320
+ <BrokenRender />
1321
+ </ErrorBoundary>,
1322
+ container,
1323
+ );
1324
+ expect(container.textContent).toBe('Caught an error: Hello.');
1325
+ expect(log).toEqual([
1326
+ 'ErrorBoundary componentWillReceiveProps',
1327
+ 'ErrorBoundary componentWillUpdate',
1328
+ 'ErrorBoundary render success',
1329
+ // BrokenRender will abort rendering:
1330
+ 'BrokenRender constructor',
1331
+ 'BrokenRender componentWillMount',
1332
+ 'BrokenRender render [!]',
1333
+ // Finish updating with null children
1334
+ 'Child1 ref is set to null',
1335
+ 'ErrorBoundary componentDidUpdate',
1336
+ // Handle the error
1337
+ 'ErrorBoundary componentDidCatch',
1338
+ 'ErrorBoundary componentWillUpdate',
1339
+ 'ErrorBoundary render error',
1340
+ 'Error message ref is set to [object HTMLDivElement]',
1341
+ // Child2 ref is never set because its mounting aborted
1342
+ 'ErrorBoundary componentDidUpdate',
1343
+ ]);
1344
+
1345
+ log.length = 0;
1346
+ ReactDOM.unmountComponentAtNode(container);
1347
+ expect(log).toEqual([
1348
+ 'ErrorBoundary componentWillUnmount',
1349
+ 'Error message ref is set to null',
1350
+ ]);
1351
+ });
1352
+
1353
+ it('recovers from componentWillUnmount errors on update', () => {
1354
+ const container = document.createElement('div');
1355
+ ReactDOM.render(
1356
+ <ErrorBoundary>
1357
+ <BrokenComponentWillUnmount />
1358
+ <BrokenComponentWillUnmount />
1359
+ <Normal />
1360
+ </ErrorBoundary>,
1361
+ container,
1362
+ );
1363
+
1364
+ log.length = 0;
1365
+ ReactDOM.render(
1366
+ <ErrorBoundary>
1367
+ <BrokenComponentWillUnmount />
1368
+ </ErrorBoundary>,
1369
+ container,
1370
+ );
1371
+ expect(container.textContent).toBe('Caught an error: Hello.');
1372
+ expect(log).toEqual([
1373
+ 'ErrorBoundary componentWillReceiveProps',
1374
+ 'ErrorBoundary componentWillUpdate',
1375
+ 'ErrorBoundary render success',
1376
+ // Update existing child:
1377
+ 'BrokenComponentWillUnmount componentWillReceiveProps',
1378
+ 'BrokenComponentWillUnmount componentWillUpdate',
1379
+ 'BrokenComponentWillUnmount render',
1380
+ // Unmounting throws:
1381
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
1382
+ // Fiber proceeds with lifecycles despite errors
1383
+ 'Normal componentWillUnmount',
1384
+ // The components have updated in this phase
1385
+ 'BrokenComponentWillUnmount componentDidUpdate',
1386
+ 'ErrorBoundary componentDidUpdate',
1387
+ // Now that commit phase is done, Fiber unmounts the boundary's children
1388
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
1389
+ 'ErrorBoundary componentDidCatch',
1390
+ // The initial render was aborted, so
1391
+ // Fiber retries from the root.
1392
+ 'ErrorBoundary componentWillUpdate',
1393
+ 'ErrorBoundary componentDidUpdate',
1394
+ // The second willUnmount error should be captured and logged, too.
1395
+ 'ErrorBoundary componentDidCatch',
1396
+ 'ErrorBoundary componentWillUpdate',
1397
+ // Render an error now (stack will do it later)
1398
+ 'ErrorBoundary render error',
1399
+ // Attempt to unmount previous child:
1400
+ // Done
1401
+ 'ErrorBoundary componentDidUpdate',
1402
+ ]);
1403
+
1404
+ log.length = 0;
1405
+ ReactDOM.unmountComponentAtNode(container);
1406
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1407
+ });
1408
+
1409
+ it('recovers from nested componentWillUnmount errors on update', () => {
1410
+ const container = document.createElement('div');
1411
+ ReactDOM.render(
1412
+ <ErrorBoundary>
1413
+ <Normal>
1414
+ <BrokenComponentWillUnmount />
1415
+ </Normal>
1416
+ <BrokenComponentWillUnmount />
1417
+ </ErrorBoundary>,
1418
+ container,
1419
+ );
1420
+
1421
+ log.length = 0;
1422
+ ReactDOM.render(
1423
+ <ErrorBoundary>
1424
+ <Normal>
1425
+ <BrokenComponentWillUnmount />
1426
+ </Normal>
1427
+ </ErrorBoundary>,
1428
+ container,
1429
+ );
1430
+ expect(container.textContent).toBe('Caught an error: Hello.');
1431
+ expect(log).toEqual([
1432
+ 'ErrorBoundary componentWillReceiveProps',
1433
+ 'ErrorBoundary componentWillUpdate',
1434
+ 'ErrorBoundary render success',
1435
+ // Update existing children:
1436
+ 'Normal componentWillReceiveProps',
1437
+ 'Normal componentWillUpdate',
1438
+ 'Normal render',
1439
+ 'BrokenComponentWillUnmount componentWillReceiveProps',
1440
+ 'BrokenComponentWillUnmount componentWillUpdate',
1441
+ 'BrokenComponentWillUnmount render',
1442
+ // Unmounting throws:
1443
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
1444
+ // Fiber proceeds with lifecycles despite errors
1445
+ 'BrokenComponentWillUnmount componentDidUpdate',
1446
+ 'Normal componentDidUpdate',
1447
+ 'ErrorBoundary componentDidUpdate',
1448
+ 'Normal componentWillUnmount',
1449
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
1450
+ // Now that commit phase is done, Fiber handles errors
1451
+ 'ErrorBoundary componentDidCatch',
1452
+ // The initial render was aborted, so
1453
+ // Fiber retries from the root.
1454
+ 'ErrorBoundary componentWillUpdate',
1455
+ 'ErrorBoundary componentDidUpdate',
1456
+ // The second willUnmount error should be captured and logged, too.
1457
+ 'ErrorBoundary componentDidCatch',
1458
+ 'ErrorBoundary componentWillUpdate',
1459
+ // Render an error now (stack will do it later)
1460
+ 'ErrorBoundary render error',
1461
+ // Done
1462
+ 'ErrorBoundary componentDidUpdate',
1463
+ ]);
1464
+
1465
+ log.length = 0;
1466
+ ReactDOM.unmountComponentAtNode(container);
1467
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1468
+ });
1469
+
1470
+ it('picks the right boundary when handling unmounting errors', () => {
1471
+ function renderInnerError(error) {
1472
+ return <div>Caught an inner error: {error.message}.</div>;
1473
+ }
1474
+ function renderOuterError(error) {
1475
+ return <div>Caught an outer error: {error.message}.</div>;
1476
+ }
1477
+
1478
+ const container = document.createElement('div');
1479
+ ReactDOM.render(
1480
+ <ErrorBoundary
1481
+ logName="OuterErrorBoundary"
1482
+ renderError={renderOuterError}>
1483
+ <ErrorBoundary
1484
+ logName="InnerErrorBoundary"
1485
+ renderError={renderInnerError}>
1486
+ <BrokenComponentWillUnmount />
1487
+ </ErrorBoundary>
1488
+ </ErrorBoundary>,
1489
+ container,
1490
+ );
1491
+
1492
+ log.length = 0;
1493
+ ReactDOM.render(
1494
+ <ErrorBoundary
1495
+ logName="OuterErrorBoundary"
1496
+ renderError={renderOuterError}>
1497
+ <ErrorBoundary
1498
+ logName="InnerErrorBoundary"
1499
+ renderError={renderInnerError}
1500
+ />
1501
+ </ErrorBoundary>,
1502
+ container,
1503
+ );
1504
+ expect(container.textContent).toBe('Caught an inner error: Hello.');
1505
+ expect(log).toEqual([
1506
+ // Update outer boundary
1507
+ 'OuterErrorBoundary componentWillReceiveProps',
1508
+ 'OuterErrorBoundary componentWillUpdate',
1509
+ 'OuterErrorBoundary render success',
1510
+ // Update inner boundary
1511
+ 'InnerErrorBoundary componentWillReceiveProps',
1512
+ 'InnerErrorBoundary componentWillUpdate',
1513
+ 'InnerErrorBoundary render success',
1514
+ // Try unmounting child
1515
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
1516
+ // Fiber proceeds with lifecycles despite errors
1517
+ // Inner and outer boundaries have updated in this phase
1518
+ 'InnerErrorBoundary componentDidUpdate',
1519
+ 'OuterErrorBoundary componentDidUpdate',
1520
+ // Now that commit phase is done, Fiber handles errors
1521
+ // Only inner boundary receives the error:
1522
+ 'InnerErrorBoundary componentDidCatch',
1523
+ 'InnerErrorBoundary componentWillUpdate',
1524
+ // Render an error now
1525
+ 'InnerErrorBoundary render error',
1526
+ // In Fiber, this was a local update to the
1527
+ // inner boundary so only its hook fires
1528
+ 'InnerErrorBoundary componentDidUpdate',
1529
+ ]);
1530
+
1531
+ log.length = 0;
1532
+ ReactDOM.unmountComponentAtNode(container);
1533
+ expect(log).toEqual([
1534
+ 'OuterErrorBoundary componentWillUnmount',
1535
+ 'InnerErrorBoundary componentWillUnmount',
1536
+ ]);
1537
+ });
1538
+
1539
+ it('can recover from error state', () => {
1540
+ const container = document.createElement('div');
1541
+ ReactDOM.render(
1542
+ <ErrorBoundary>
1543
+ <BrokenRender />
1544
+ </ErrorBoundary>,
1545
+ container,
1546
+ );
1547
+
1548
+ ReactDOM.render(
1549
+ <ErrorBoundary>
1550
+ <Normal />
1551
+ </ErrorBoundary>,
1552
+ container,
1553
+ );
1554
+ // Error boundary doesn't retry by itself:
1555
+ expect(container.textContent).toBe('Caught an error: Hello.');
1556
+
1557
+ // Force the success path:
1558
+ log.length = 0;
1559
+ ReactDOM.render(
1560
+ <ErrorBoundary forceRetry={true}>
1561
+ <Normal />
1562
+ </ErrorBoundary>,
1563
+ container,
1564
+ );
1565
+ expect(container.textContent).not.toContain('Caught an error');
1566
+ expect(log).toEqual([
1567
+ 'ErrorBoundary componentWillReceiveProps',
1568
+ 'ErrorBoundary componentWillUpdate',
1569
+ 'ErrorBoundary render success',
1570
+ // Mount children:
1571
+ 'Normal constructor',
1572
+ 'Normal componentWillMount',
1573
+ 'Normal render',
1574
+ // Finalize updates:
1575
+ 'Normal componentDidMount',
1576
+ 'ErrorBoundary componentDidUpdate',
1577
+ ]);
1578
+
1579
+ log.length = 0;
1580
+ ReactDOM.unmountComponentAtNode(container);
1581
+ expect(log).toEqual([
1582
+ 'ErrorBoundary componentWillUnmount',
1583
+ 'Normal componentWillUnmount',
1584
+ ]);
1585
+ });
1586
+
1587
+ it('can update multiple times in error state', () => {
1588
+ const container = document.createElement('div');
1589
+ ReactDOM.render(
1590
+ <ErrorBoundary>
1591
+ <BrokenRender />
1592
+ </ErrorBoundary>,
1593
+ container,
1594
+ );
1595
+ expect(container.textContent).toBe('Caught an error: Hello.');
1596
+
1597
+ ReactDOM.render(
1598
+ <ErrorBoundary>
1599
+ <BrokenRender />
1600
+ </ErrorBoundary>,
1601
+ container,
1602
+ );
1603
+ expect(container.textContent).toBe('Caught an error: Hello.');
1604
+
1605
+ ReactDOM.render(<div>Other screen</div>, container);
1606
+ expect(container.textContent).toBe('Other screen');
1607
+
1608
+ ReactDOM.unmountComponentAtNode(container);
1609
+ });
1610
+
1611
+ it("doesn't get into inconsistent state during removals", () => {
1612
+ const container = document.createElement('div');
1613
+ ReactDOM.render(
1614
+ <ErrorBoundary>
1615
+ <Normal />
1616
+ <BrokenComponentWillUnmount />
1617
+ <Normal />
1618
+ </ErrorBoundary>,
1619
+ container,
1620
+ );
1621
+
1622
+ ReactDOM.render(<ErrorBoundary />, container);
1623
+ expect(container.textContent).toBe('Caught an error: Hello.');
1624
+
1625
+ log.length = 0;
1626
+ ReactDOM.unmountComponentAtNode(container);
1627
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1628
+ });
1629
+
1630
+ it("doesn't get into inconsistent state during additions", () => {
1631
+ const container = document.createElement('div');
1632
+ ReactDOM.render(<ErrorBoundary />, container);
1633
+ ReactDOM.render(
1634
+ <ErrorBoundary>
1635
+ <Normal />
1636
+ <BrokenRender />
1637
+ <Normal />
1638
+ </ErrorBoundary>,
1639
+ container,
1640
+ );
1641
+ expect(container.textContent).toBe('Caught an error: Hello.');
1642
+
1643
+ log.length = 0;
1644
+ ReactDOM.unmountComponentAtNode(container);
1645
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1646
+ });
1647
+
1648
+ it("doesn't get into inconsistent state during reorders", () => {
1649
+ function getAMixOfNormalAndBrokenRenderElements() {
1650
+ const elements = [];
1651
+ for (let i = 0; i < 100; i++) {
1652
+ elements.push(<Normal key={i} />);
1653
+ }
1654
+ elements.push(<MaybeBrokenRender key={100} />);
1655
+
1656
+ let currentIndex = elements.length;
1657
+ while (0 !== currentIndex) {
1658
+ const randomIndex = Math.floor(Math.random() * currentIndex);
1659
+ currentIndex -= 1;
1660
+ const temporaryValue = elements[currentIndex];
1661
+ elements[currentIndex] = elements[randomIndex];
1662
+ elements[randomIndex] = temporaryValue;
1663
+ }
1664
+ return elements;
1665
+ }
1666
+
1667
+ class MaybeBrokenRender extends React.Component {
1668
+ render() {
1669
+ if (fail) {
1670
+ throw new Error('Hello');
1671
+ }
1672
+ return <div>{this.props.children}</div>;
1673
+ }
1674
+ }
1675
+
1676
+ let fail = false;
1677
+ const container = document.createElement('div');
1678
+ ReactDOM.render(
1679
+ <ErrorBoundary>{getAMixOfNormalAndBrokenRenderElements()}</ErrorBoundary>,
1680
+ container,
1681
+ );
1682
+ expect(container.textContent).not.toContain('Caught an error');
1683
+
1684
+ fail = true;
1685
+ ReactDOM.render(
1686
+ <ErrorBoundary>{getAMixOfNormalAndBrokenRenderElements()}</ErrorBoundary>,
1687
+ container,
1688
+ );
1689
+ expect(container.textContent).toBe('Caught an error: Hello.');
1690
+
1691
+ log.length = 0;
1692
+ ReactDOM.unmountComponentAtNode(container);
1693
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1694
+ });
1695
+
1696
+ it('catches errors originating downstream', () => {
1697
+ let fail = false;
1698
+ class Stateful extends React.Component {
1699
+ state = {shouldThrow: false};
1700
+
1701
+ render() {
1702
+ if (fail) {
1703
+ log.push('Stateful render [!]');
1704
+ throw new Error('Hello');
1705
+ }
1706
+ return <div>{this.props.children}</div>;
1707
+ }
1708
+ }
1709
+
1710
+ let statefulInst;
1711
+ const container = document.createElement('div');
1712
+ ReactDOM.render(
1713
+ <ErrorBoundary>
1714
+ <Stateful ref={inst => (statefulInst = inst)} />
1715
+ </ErrorBoundary>,
1716
+ container,
1717
+ );
1718
+
1719
+ log.length = 0;
1720
+ expect(() => {
1721
+ fail = true;
1722
+ statefulInst.forceUpdate();
1723
+ }).not.toThrow();
1724
+
1725
+ expect(log).toEqual([
1726
+ 'Stateful render [!]',
1727
+ 'ErrorBoundary componentDidCatch',
1728
+ 'ErrorBoundary componentWillUpdate',
1729
+ 'ErrorBoundary render error',
1730
+ 'ErrorBoundary componentDidUpdate',
1731
+ ]);
1732
+
1733
+ log.length = 0;
1734
+ ReactDOM.unmountComponentAtNode(container);
1735
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1736
+ });
1737
+
1738
+ it('catches errors in componentDidMount', () => {
1739
+ const container = document.createElement('div');
1740
+ ReactDOM.render(
1741
+ <ErrorBoundary>
1742
+ <BrokenComponentWillUnmount>
1743
+ <Normal />
1744
+ </BrokenComponentWillUnmount>
1745
+ <BrokenComponentDidMount />
1746
+ <Normal logName="LastChild" />
1747
+ </ErrorBoundary>,
1748
+ container,
1749
+ );
1750
+ expect(log).toEqual([
1751
+ 'ErrorBoundary constructor',
1752
+ 'ErrorBoundary componentWillMount',
1753
+ 'ErrorBoundary render success',
1754
+ 'BrokenComponentWillUnmount constructor',
1755
+ 'BrokenComponentWillUnmount componentWillMount',
1756
+ 'BrokenComponentWillUnmount render',
1757
+ 'Normal constructor',
1758
+ 'Normal componentWillMount',
1759
+ 'Normal render',
1760
+ 'BrokenComponentDidMount constructor',
1761
+ 'BrokenComponentDidMount componentWillMount',
1762
+ 'BrokenComponentDidMount render',
1763
+ 'LastChild constructor',
1764
+ 'LastChild componentWillMount',
1765
+ 'LastChild render',
1766
+ // Start flushing didMount queue
1767
+ 'Normal componentDidMount',
1768
+ 'BrokenComponentWillUnmount componentDidMount',
1769
+ 'BrokenComponentDidMount componentDidMount [!]',
1770
+ // Continue despite the error
1771
+ 'LastChild componentDidMount',
1772
+ 'ErrorBoundary componentDidMount',
1773
+ // Now we are ready to handle the error
1774
+ // Safely unmount every child
1775
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
1776
+ // Continue unmounting safely despite any errors
1777
+ 'Normal componentWillUnmount',
1778
+ 'BrokenComponentDidMount componentWillUnmount',
1779
+ 'LastChild componentWillUnmount',
1780
+ // Handle the error
1781
+ 'ErrorBoundary componentDidCatch',
1782
+ 'ErrorBoundary componentWillUpdate',
1783
+ // The willUnmount error should be captured and logged, too.
1784
+ 'ErrorBoundary componentDidUpdate',
1785
+ 'ErrorBoundary componentDidCatch',
1786
+ 'ErrorBoundary componentWillUpdate',
1787
+ 'ErrorBoundary render error',
1788
+ // The update has finished
1789
+ 'ErrorBoundary componentDidUpdate',
1790
+ ]);
1791
+
1792
+ log.length = 0;
1793
+ ReactDOM.unmountComponentAtNode(container);
1794
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1795
+ });
1796
+
1797
+ it('catches errors in componentDidUpdate', () => {
1798
+ const container = document.createElement('div');
1799
+ ReactDOM.render(
1800
+ <ErrorBoundary>
1801
+ <BrokenComponentDidUpdate />
1802
+ </ErrorBoundary>,
1803
+ container,
1804
+ );
1805
+
1806
+ log.length = 0;
1807
+ ReactDOM.render(
1808
+ <ErrorBoundary>
1809
+ <BrokenComponentDidUpdate />
1810
+ </ErrorBoundary>,
1811
+ container,
1812
+ );
1813
+ expect(log).toEqual([
1814
+ 'ErrorBoundary componentWillReceiveProps',
1815
+ 'ErrorBoundary componentWillUpdate',
1816
+ 'ErrorBoundary render success',
1817
+ 'BrokenComponentDidUpdate componentWillReceiveProps',
1818
+ 'BrokenComponentDidUpdate componentWillUpdate',
1819
+ 'BrokenComponentDidUpdate render',
1820
+ // All lifecycles run
1821
+ 'BrokenComponentDidUpdate componentDidUpdate [!]',
1822
+ 'ErrorBoundary componentDidUpdate',
1823
+ 'BrokenComponentDidUpdate componentWillUnmount',
1824
+ // Then, error is handled
1825
+ 'ErrorBoundary componentDidCatch',
1826
+ 'ErrorBoundary componentWillUpdate',
1827
+ 'ErrorBoundary render error',
1828
+ 'ErrorBoundary componentDidUpdate',
1829
+ ]);
1830
+
1831
+ log.length = 0;
1832
+ ReactDOM.unmountComponentAtNode(container);
1833
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1834
+ });
1835
+
1836
+ it('propagates errors inside boundary during componentDidMount', () => {
1837
+ const container = document.createElement('div');
1838
+ ReactDOM.render(
1839
+ <ErrorBoundary>
1840
+ <BrokenComponentDidMountErrorBoundary
1841
+ renderError={error => (
1842
+ <div>We should never catch our own error: {error.message}.</div>
1843
+ )}
1844
+ />
1845
+ </ErrorBoundary>,
1846
+ container,
1847
+ );
1848
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
1849
+ expect(log).toEqual([
1850
+ 'ErrorBoundary constructor',
1851
+ 'ErrorBoundary componentWillMount',
1852
+ 'ErrorBoundary render success',
1853
+ 'BrokenComponentDidMountErrorBoundary constructor',
1854
+ 'BrokenComponentDidMountErrorBoundary componentWillMount',
1855
+ 'BrokenComponentDidMountErrorBoundary render success',
1856
+ 'BrokenComponentDidMountErrorBoundary componentDidMount [!]',
1857
+ // Fiber proceeds with the hooks
1858
+ 'ErrorBoundary componentDidMount',
1859
+ 'BrokenComponentDidMountErrorBoundary componentWillUnmount',
1860
+ // The error propagates to the higher boundary
1861
+ 'ErrorBoundary componentDidCatch',
1862
+ // Fiber retries from the root
1863
+ 'ErrorBoundary componentWillUpdate',
1864
+ 'ErrorBoundary render error',
1865
+ 'ErrorBoundary componentDidUpdate',
1866
+ ]);
1867
+
1868
+ log.length = 0;
1869
+ ReactDOM.unmountComponentAtNode(container);
1870
+ expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
1871
+ });
1872
+
1873
+ it('calls componentDidCatch for each error that is captured', () => {
1874
+ function renderUnmountError(error) {
1875
+ return <div>Caught an unmounting error: {error.message}.</div>;
1876
+ }
1877
+ function renderUpdateError(error) {
1878
+ return <div>Caught an updating error: {error.message}.</div>;
1879
+ }
1880
+
1881
+ const container = document.createElement('div');
1882
+ ReactDOM.render(
1883
+ <ErrorBoundary logName="OuterErrorBoundary">
1884
+ <ErrorBoundary
1885
+ logName="InnerUnmountBoundary"
1886
+ renderError={renderUnmountError}>
1887
+ <BrokenComponentWillUnmount errorText="E1" />
1888
+ <BrokenComponentWillUnmount errorText="E2" />
1889
+ </ErrorBoundary>
1890
+ <ErrorBoundary
1891
+ logName="InnerUpdateBoundary"
1892
+ renderError={renderUpdateError}>
1893
+ <BrokenComponentDidUpdate errorText="E3" />
1894
+ <BrokenComponentDidUpdate errorText="E4" />
1895
+ </ErrorBoundary>
1896
+ </ErrorBoundary>,
1897
+ container,
1898
+ );
1899
+
1900
+ log.length = 0;
1901
+ ReactDOM.render(
1902
+ <ErrorBoundary logName="OuterErrorBoundary">
1903
+ <ErrorBoundary
1904
+ logName="InnerUnmountBoundary"
1905
+ renderError={renderUnmountError}
1906
+ />
1907
+ <ErrorBoundary
1908
+ logName="InnerUpdateBoundary"
1909
+ renderError={renderUpdateError}>
1910
+ <BrokenComponentDidUpdate errorText="E3" />
1911
+ <BrokenComponentDidUpdate errorText="E4" />
1912
+ </ErrorBoundary>
1913
+ </ErrorBoundary>,
1914
+ container,
1915
+ );
1916
+
1917
+ expect(container.firstChild.textContent).toBe(
1918
+ 'Caught an unmounting error: E2.' + 'Caught an updating error: E4.',
1919
+ );
1920
+ expect(log).toEqual([
1921
+ // Begin update phase
1922
+ 'OuterErrorBoundary componentWillReceiveProps',
1923
+ 'OuterErrorBoundary componentWillUpdate',
1924
+ 'OuterErrorBoundary render success',
1925
+ 'InnerUnmountBoundary componentWillReceiveProps',
1926
+ 'InnerUnmountBoundary componentWillUpdate',
1927
+ 'InnerUnmountBoundary render success',
1928
+ 'InnerUpdateBoundary componentWillReceiveProps',
1929
+ 'InnerUpdateBoundary componentWillUpdate',
1930
+ 'InnerUpdateBoundary render success',
1931
+ // First come the updates
1932
+ 'BrokenComponentDidUpdate componentWillReceiveProps',
1933
+ 'BrokenComponentDidUpdate componentWillUpdate',
1934
+ 'BrokenComponentDidUpdate render',
1935
+ 'BrokenComponentDidUpdate componentWillReceiveProps',
1936
+ 'BrokenComponentDidUpdate componentWillUpdate',
1937
+ 'BrokenComponentDidUpdate render',
1938
+ // We're in commit phase now, deleting
1939
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
1940
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
1941
+ // Continue despite errors, handle them after commit is done
1942
+ 'InnerUnmountBoundary componentDidUpdate',
1943
+ // We're still in commit phase, now calling update lifecycles
1944
+ 'BrokenComponentDidUpdate componentDidUpdate [!]',
1945
+ // Again, continue despite errors, we'll handle them later
1946
+ 'BrokenComponentDidUpdate componentDidUpdate [!]',
1947
+ 'InnerUpdateBoundary componentDidUpdate',
1948
+ 'OuterErrorBoundary componentDidUpdate',
1949
+ // After the commit phase, attempt to recover from any errors that
1950
+ // were captured
1951
+ 'BrokenComponentDidUpdate componentWillUnmount',
1952
+ 'BrokenComponentDidUpdate componentWillUnmount',
1953
+ 'InnerUnmountBoundary componentDidCatch',
1954
+ 'InnerUnmountBoundary componentDidCatch',
1955
+ 'InnerUpdateBoundary componentDidCatch',
1956
+ 'InnerUpdateBoundary componentDidCatch',
1957
+ 'InnerUnmountBoundary componentWillUpdate',
1958
+ 'InnerUnmountBoundary render error',
1959
+ 'InnerUpdateBoundary componentWillUpdate',
1960
+ 'InnerUpdateBoundary render error',
1961
+ 'InnerUnmountBoundary componentDidUpdate',
1962
+ 'InnerUpdateBoundary componentDidUpdate',
1963
+ ]);
1964
+
1965
+ log.length = 0;
1966
+ ReactDOM.unmountComponentAtNode(container);
1967
+ expect(log).toEqual([
1968
+ 'OuterErrorBoundary componentWillUnmount',
1969
+ 'InnerUnmountBoundary componentWillUnmount',
1970
+ 'InnerUpdateBoundary componentWillUnmount',
1971
+ ]);
1972
+ });
1973
+
1974
+ it('discards a bad root if the root component fails', () => {
1975
+ const X = null;
1976
+ const Y = undefined;
1977
+ let err1;
1978
+ let err2;
1979
+
1980
+ try {
1981
+ let container = document.createElement('div');
1982
+ expect(() => ReactDOM.render(<X />, container)).toWarnDev(
1983
+ 'React.createElement: type is invalid -- expected a string ' +
1984
+ '(for built-in components) or a class/function ' +
1985
+ '(for composite components) but got: null.',
1986
+ );
1987
+ } catch (err) {
1988
+ err1 = err;
1989
+ }
1990
+ try {
1991
+ let container = document.createElement('div');
1992
+ expect(() => ReactDOM.render(<Y />, container)).toWarnDev(
1993
+ 'React.createElement: type is invalid -- expected a string ' +
1994
+ '(for built-in components) or a class/function ' +
1995
+ '(for composite components) but got: undefined.',
1996
+ );
1997
+ } catch (err) {
1998
+ err2 = err;
1999
+ }
2000
+
2001
+ expect(err1.message).toMatch(/got: null/);
2002
+ expect(err2.message).toMatch(/got: undefined/);
2003
+ });
2004
+
2005
+ it('renders empty output if error boundary does not handle the error', () => {
2006
+ const container = document.createElement('div');
2007
+ expect(() => {
2008
+ ReactDOM.render(
2009
+ <div>
2010
+ Sibling
2011
+ <NoopErrorBoundary>
2012
+ <BrokenRender />
2013
+ </NoopErrorBoundary>
2014
+ </div>,
2015
+ container,
2016
+ );
2017
+ }).toWarnDev(
2018
+ 'ErrorBoundary: Error boundaries should implement getDerivedStateFromError()',
2019
+ {withoutStack: true},
2020
+ );
2021
+ expect(container.firstChild.textContent).toBe('Sibling');
2022
+ expect(log).toEqual([
2023
+ 'NoopErrorBoundary constructor',
2024
+ 'NoopErrorBoundary componentWillMount',
2025
+ 'NoopErrorBoundary render',
2026
+ 'BrokenRender constructor',
2027
+ 'BrokenRender componentWillMount',
2028
+ 'BrokenRender render [!]',
2029
+ // In Fiber, noop error boundaries render null
2030
+ 'NoopErrorBoundary componentDidMount',
2031
+ 'NoopErrorBoundary componentDidCatch',
2032
+ // Nothing happens.
2033
+ ]);
2034
+
2035
+ log.length = 0;
2036
+ ReactDOM.unmountComponentAtNode(container);
2037
+ expect(log).toEqual(['NoopErrorBoundary componentWillUnmount']);
2038
+ });
2039
+
2040
+ it('passes first error when two errors happen in commit', () => {
2041
+ const errors = [];
2042
+ let caughtError;
2043
+ class Parent extends React.Component {
2044
+ render() {
2045
+ return <Child />;
2046
+ }
2047
+ componentDidMount() {
2048
+ errors.push('parent sad');
2049
+ throw new Error('parent sad');
2050
+ }
2051
+ }
2052
+ class Child extends React.Component {
2053
+ render() {
2054
+ return <div />;
2055
+ }
2056
+ componentDidMount() {
2057
+ errors.push('child sad');
2058
+ throw new Error('child sad');
2059
+ }
2060
+ }
2061
+
2062
+ const container = document.createElement('div');
2063
+ try {
2064
+ // Here, we test the behavior where there is no error boundary and we
2065
+ // delegate to the host root.
2066
+ ReactDOM.render(<Parent />, container);
2067
+ } catch (e) {
2068
+ if (e.message !== 'parent sad' && e.message !== 'child sad') {
2069
+ throw e;
2070
+ }
2071
+ caughtError = e;
2072
+ }
2073
+
2074
+ expect(errors).toEqual(['child sad', 'parent sad']);
2075
+ // Error should be the first thrown
2076
+ expect(caughtError.message).toBe('child sad');
2077
+ });
2078
+
2079
+ it('propagates uncaught error inside unbatched initial mount', () => {
2080
+ function Foo() {
2081
+ throw new Error('foo error');
2082
+ }
2083
+ const container = document.createElement('div');
2084
+ expect(() => {
2085
+ ReactDOM.unstable_batchedUpdates(() => {
2086
+ ReactDOM.render(<Foo />, container);
2087
+ });
2088
+ }).toThrow('foo error');
2089
+ });
2090
+
2091
+ it('handles errors that occur in before-mutation commit hook', () => {
2092
+ const errors = [];
2093
+ let caughtError;
2094
+ class Parent extends React.Component {
2095
+ getSnapshotBeforeUpdate() {
2096
+ errors.push('parent sad');
2097
+ throw new Error('parent sad');
2098
+ }
2099
+ componentDidUpdate() {}
2100
+ render() {
2101
+ return <Child {...this.props} />;
2102
+ }
2103
+ }
2104
+ class Child extends React.Component {
2105
+ getSnapshotBeforeUpdate() {
2106
+ errors.push('child sad');
2107
+ throw new Error('child sad');
2108
+ }
2109
+ componentDidUpdate() {}
2110
+ render() {
2111
+ return <div />;
2112
+ }
2113
+ }
2114
+
2115
+ const container = document.createElement('div');
2116
+ ReactDOM.render(<Parent value={1} />, container);
2117
+ try {
2118
+ ReactDOM.render(<Parent value={2} />, container);
2119
+ } catch (e) {
2120
+ if (e.message !== 'parent sad' && e.message !== 'child sad') {
2121
+ throw e;
2122
+ }
2123
+ caughtError = e;
2124
+ }
2125
+
2126
+ expect(errors).toEqual(['child sad', 'parent sad']);
2127
+ // Error should be the first thrown
2128
+ expect(caughtError.message).toBe('child sad');
2129
+ });
2130
+});
packages/react-dom/src/__tests__/ReactUpdates-test.js
+3
@@ -1343,6 +1343,9 @@ describe('ReactUpdates', () => {
1343
1344
class ErrorBoundary extends React.Component {
1345
componentDidCatch() {
1346
+ // Schedule a no-op state update to avoid triggering a DEV warning in the test.
1347
+ this.setState({});
1348
+
1349
this.props.parent.remount();
1350
}
1351
render() {
packages/react-reconciler/src/ReactFiberBeginWork.js
+71
-29
@@ -46,7 +46,6 @@ import {
46
import {captureWillSyncRenderPlaceholder} from './ReactFiberScheduler';
47
import ReactSharedInternals from 'shared/ReactSharedInternals';
48
import {
49
- enableGetDerivedStateFromCatch,
49
enableSuspense,
50
debugRenderPhaseSideEffects,
51
debugRenderPhaseSideEffectsForStrictMode,
@@ -156,6 +155,38 @@ export function reconcileChildren(
155
}
156
}
157
158
+function forceUnmountCurrentAndReconcile(
159
+ current: Fiber,
160
+ workInProgress: Fiber,
161
+ nextChildren: any,
162
+ renderExpirationTime: ExpirationTime,
163
+) {
164
+ // This function is fork of reconcileChildren. It's used in cases where we
165
+ // want to reconcile without matching against the existing set. This has the
166
+ // effect of all current children being unmounted; even if the type and key
167
+ // are the same, the old child is unmounted and a new child is created.
168
+ //
169
+ // To do this, we're going to go through the reconcile algorithm twice. In
170
+ // the first pass, we schedule a deletion for all the current children by
171
+ // passing null.
172
+ workInProgress.child = reconcileChildFibers(
173
+ workInProgress,
174
+ current.child,
175
+ null,
176
+ renderExpirationTime,
177
+ );
178
+ // In the second pass, we mount the new children. The trick here is that we
179
+ // pass null in place of where we usually pass the current child set. This has
180
+ // the effect of remounting all children regardless of whether their their
181
+ // identity matches.
182
+ workInProgress.child = reconcileChildFibers(
183
+ workInProgress,
184
+ null,
185
+ nextChildren,
186
+ renderExpirationTime,
187
+ );
188
+}
189
+
190
function updateForwardRef(
191
current: Fiber | null,
192
workInProgress: Fiber,
@@ -444,8 +475,7 @@ function finishClassComponent(
475
let nextChildren;
476
if (
477
didCaptureError &&
447
- (!enableGetDerivedStateFromCatch ||
448
- typeof Component.getDerivedStateFromCatch !== 'function')
478
+ typeof Component.getDerivedStateFromError !== 'function'
479
) {
480
// If we captured an error, but getDerivedStateFrom catch is not defined,
481
// unmount all the children. componentDidCatch will schedule an update to
@@ -477,20 +507,25 @@ function finishClassComponent(
507
// React DevTools reads this flag.
508
workInProgress.effectTag |= PerformedWork;
509
if (current !== null && didCaptureError) {
480
- // If we're recovering from an error, reconcile twice: first to delete
481
- // all the existing children.
482
- reconcileChildren(current, workInProgress, null, renderExpirationTime);
483
- workInProgress.child = null;
484
- // Now we can continue reconciling like normal. This has the effect of
485
- // remounting all children regardless of whether their their
486
- // identity matches.
510
+ // If we're recovering from an error, reconcile without reusing any of
511
+ // the existing children. Conceptually, the normal children and the children
512
+ // that are shown on error are two different sets, so we shouldn't reuse
513
+ // normal children even if their identities match.
514
+ forceUnmountCurrentAndReconcile(
515
+ current,
516
+ workInProgress,
517
+ nextChildren,
518
+ renderExpirationTime,
519
+ );
520
+ } else {
521
+ reconcileChildren(
522
+ current,
523
+ workInProgress,
524
+ nextChildren,
525
+ renderExpirationTime,
526
+ );
527
}
488
- reconcileChildren(
489
- current,
490
- workInProgress,
491
- nextChildren,
492
- renderExpirationTime,
493
- );
528
+
529
// Memoize props and state using the values we just used to render.
530
// TODO: Restructure so we never read values from the instance.
531
memoizeState(workInProgress, instance.state);
@@ -930,13 +965,6 @@ function updatePlaceholderComponent(
965
// suspended during the last commit. Switch to the placholder.
966
workInProgress.updateQueue = null;
967
nextDidTimeout = true;
933
- // If we're recovering from an error, reconcile twice: first to delete
934
- // all the existing children.
935
- reconcileChildren(current, workInProgress, null, renderExpirationTime);
936
- current.child = null;
937
- // Now we can continue reconciling like normal. This has the effect of
938
- // remounting all children regardless of whether their their
939
- // identity matches.
968
} else {
969
nextDidTimeout = !alreadyCaptured;
970
}
@@ -963,14 +991,28 @@ function updatePlaceholderComponent(
991
nextChildren = nextDidTimeout ? nextProps.fallback : children;
992
}
993
994
+ if (current !== null && nextDidTimeout !== workInProgress.memoizedState) {
995
+ // We're about to switch from the placeholder children to the normal
996
+ // children, or vice versa. These are two different conceptual sets that
997
+ // happen to be stored in the same set. Call this special function to
998
+ // force the new set not to match with the current set.
999
+ // TODO: The proper way to model this is by storing each set separately.
1000
+ forceUnmountCurrentAndReconcile(
1001
+ current,
1002
+ workInProgress,
1003
+ nextChildren,
1004
+ renderExpirationTime,
1005
+ );
1006
+ } else {
1007
+ reconcileChildren(
1008
+ current,
1009
+ workInProgress,
1010
+ nextChildren,
1011
+ renderExpirationTime,
1012
+ );
1013
+ }
1014
workInProgress.memoizedProps = nextProps;
1015
workInProgress.memoizedState = nextDidTimeout;
968
- reconcileChildren(
969
- current,
970
- workInProgress,
971
- nextChildren,
972
- renderExpirationTime,
973
- );
1016
return workInProgress.child;
1017
} else {
1018
return null;
packages/react-reconciler/src/ReactFiberClassComponent.js
+2
-2
@@ -466,10 +466,10 @@ function checkClassInstance(workInProgress: Fiber, ctor: any, newProps: any) {
466
name,
467
);
468
const noInstanceGetDerivedStateFromCatch =
469
- typeof instance.getDerivedStateFromCatch !== 'function';
469
+ typeof instance.getDerivedStateFromError !== 'function';
470
warningWithoutStack(
471
noInstanceGetDerivedStateFromCatch,
472
- '%s: getDerivedStateFromCatch() is defined as an instance method ' +
472
+ '%s: getDerivedStateFromError() is defined as an instance method ' +
473
'and will be ignored. Instead, declare it as a static method.',
474
name,
475
);
packages/react-reconciler/src/ReactFiberScheduler.js
+1
-1
@@ -1464,7 +1464,7 @@ function dispatch(
1464
const ctor = fiber.type;
1465
const instance = fiber.stateNode;
1466
if (
1467
- typeof ctor.getDerivedStateFromCatch === 'function' ||
1467
+ typeof ctor.getDerivedStateFromError === 'function' ||
1468
(typeof instance.componentDidCatch === 'function' &&
1469
!isAlreadyFailedLegacyErrorBoundary(instance))
1470
) {
packages/react-reconciler/src/ReactFiberUnwindWork.js
+22
-18
@@ -14,6 +14,8 @@ import type {CapturedValue} from './ReactCapturedValue';
14
import type {Update} from './ReactUpdateQueue';
15
import type {Thenable} from './ReactFiberScheduler';
16
17
+import getComponentName from 'shared/getComponentName';
18
+import warningWithoutStack from 'shared/warningWithoutStack';
19
import {
20
IndeterminateComponent,
21
FunctionalComponent,
@@ -33,11 +35,7 @@ import {
35
Update as UpdateEffect,
36
LifecycleEffectMask,
37
} from 'shared/ReactSideEffectTags';
36
-import {
37
- enableGetDerivedStateFromCatch,
38
- enableSuspense,
39
- enableSchedulerTracing,
40
-} from 'shared/ReactFeatureFlags';
38
+import {enableSuspense, enableSchedulerTracing} from 'shared/ReactFeatureFlags';
39
import {StrictMode, ConcurrentMode} from './ReactTypeOfMode';
40
41
import {createCapturedValue} from './ReactCapturedValue';
@@ -104,28 +102,22 @@ function createClassErrorUpdate(
102
): Update<mixed> {
103
const update = createUpdate(expirationTime);
104
update.tag = CaptureUpdate;
107
- const getDerivedStateFromCatch = fiber.type.getDerivedStateFromCatch;
108
- if (
109
- enableGetDerivedStateFromCatch &&
110
- typeof getDerivedStateFromCatch === 'function'
111
- ) {
105
+ const getDerivedStateFromError = fiber.type.getDerivedStateFromError;
106
+ if (typeof getDerivedStateFromError === 'function') {
107
const error = errorInfo.value;
108
update.payload = () => {
114
- return getDerivedStateFromCatch(error);
109
+ return getDerivedStateFromError(error);
110
};
111
}
112
113
const inst = fiber.stateNode;
114
if (inst !== null && typeof inst.componentDidCatch === 'function') {
115
update.callback = function callback() {
121
- if (
122
- !enableGetDerivedStateFromCatch ||
123
- getDerivedStateFromCatch !== 'function'
124
- ) {
116
+ if (typeof getDerivedStateFromError !== 'function') {
117
// To preserve the preexisting retry behavior of error boundaries,
118
// we keep track of which ones already failed during this batch.
119
// This gets reset before we yield back to the browser.
128
- // TODO: Warn in strict mode if getDerivedStateFromCatch is
120
+ // TODO: Warn in strict mode if getDerivedStateFromError is
121
// not defined.
122
markLegacyErrorBoundaryAsFailed(this);
123
}
@@ -135,6 +127,19 @@ function createClassErrorUpdate(
127
this.componentDidCatch(error, {
128
componentStack: stack !== null ? stack : '',
129
});
130
+ if (__DEV__) {
131
+ if (typeof getDerivedStateFromError !== 'function') {
132
+ // If componentDidCatch is the only error boundary method defined,
133
+ // then it needs to call setState to recover from errors.
134
+ // If no state update is scheduled then the boundary will swallow the error.
135
+ warningWithoutStack(
136
+ fiber.expirationTime === Sync,
137
+ '%s: Error boundaries should implement getDerivedStateFromError(). ' +
138
+ 'In that method, return a state update to display an error message or fallback UI.',
139
+ getComponentName(fiber.type) || 'Unknown',
140
+ );
141
+ }
142
+ }
143
};
144
}
145
return update;
@@ -364,8 +369,7 @@ function throwException(
369
const instance = workInProgress.stateNode;
370
if (
371
(workInProgress.effectTag & DidCapture) === NoEffect &&
367
- ((typeof ctor.getDerivedStateFromCatch === 'function' &&
368
- enableGetDerivedStateFromCatch) ||
372
+ (typeof ctor.getDerivedStateFromError === 'function' ||
373
(instance !== null &&
374
typeof instance.componentDidCatch === 'function' &&
375
!isAlreadyFailedLegacyErrorBoundary(instance)))
packages/react-reconciler/src/__tests__/ErrorBoundaryReconciliation-test.internal.js
new
+111
@@ -0,0 +1,111 @@
1
+const jestDiff = require('jest-diff');
2
+
3
+describe('ErrorBoundaryReconciliation', () => {
4
+ let BrokenRender;
5
+ let DidCatchErrorBoundary;
6
+ let GetDerivedErrorBoundary;
7
+ let React;
8
+ let ReactFeatureFlags;
9
+ let ReactTestRenderer;
10
+ let span;
11
+
12
+ beforeEach(() => {
13
+ jest.resetModules();
14
+
15
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
16
+ ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
17
+ ReactTestRenderer = require('react-test-renderer');
18
+ React = require('react');
19
+
20
+ DidCatchErrorBoundary = class extends React.Component {
21
+ state = {error: null};
22
+ componentDidCatch(error) {
23
+ this.setState({error});
24
+ }
25
+ render() {
26
+ return this.state.error
27
+ ? React.createElement(this.props.fallbackTagName, {
28
+ prop: 'ErrorBoundary',
29
+ })
30
+ : this.props.children;
31
+ }
32
+ };
33
+
34
+ GetDerivedErrorBoundary = class extends React.Component {
35
+ state = {error: null};
36
+ static getDerivedStateFromError(error) {
37
+ return {error};
38
+ }
39
+ render() {
40
+ return this.state.error
41
+ ? React.createElement(this.props.fallbackTagName, {
42
+ prop: 'ErrorBoundary',
43
+ })
44
+ : this.props.children;
45
+ }
46
+ };
47
+
48
+ const InvalidType = undefined;
49
+ BrokenRender = ({fail}) =>
50
+ fail ? <InvalidType /> : <span prop="BrokenRender" />;
51
+
52
+ function toHaveRenderedChildren(renderer, children) {
53
+ let actual, expected;
54
+ try {
55
+ actual = renderer.toJSON();
56
+ expected = ReactTestRenderer.create(children).toJSON();
57
+ expect(actual).toEqual(expected);
58
+ } catch (error) {
59
+ return {
60
+ message: () => jestDiff(expected, actual),
61
+ pass: false,
62
+ };
63
+ }
64
+ return {pass: true};
65
+ }
66
+ expect.extend({toHaveRenderedChildren});
67
+ });
68
+
69
+ [true, false].forEach(isConcurrent => {
70
+ function sharedTest(ErrorBoundary, fallbackTagName) {
71
+ const renderer = ReactTestRenderer.create(
72
+ <ErrorBoundary fallbackTagName={fallbackTagName}>
73
+ <BrokenRender fail={false} />
74
+ </ErrorBoundary>,
75
+ {unstable_isConcurrent: isConcurrent},
76
+ );
77
+ if (isConcurrent) {
78
+ renderer.unstable_flushAll();
79
+ }
80
+ expect(renderer).toHaveRenderedChildren(<span prop="BrokenRender" />);
81
+
82
+ expect(() => {
83
+ renderer.update(
84
+ <ErrorBoundary fallbackTagName={fallbackTagName}>
85
+ <BrokenRender fail={true} />
86
+ </ErrorBoundary>,
87
+ );
88
+ if (isConcurrent) {
89
+ renderer.unstable_flushAll();
90
+ }
91
+ }).toWarnDev(isConcurrent ? ['invalid', 'invalid'] : ['invalid']);
92
+ expect(renderer).toHaveRenderedChildren(
93
+ React.createElement(fallbackTagName, {prop: 'ErrorBoundary'}),
94
+ );
95
+ }
96
+
97
+ describe(isConcurrent ? 'concurrent' : 'sync', () => {
98
+ it('componentDidCatch can recover by rendering an element of the same type', () =>
99
+ sharedTest(DidCatchErrorBoundary, 'span'));
100
+
101
+ it('componentDidCatch can recover by rendering an element of a different type', () =>
102
+ sharedTest(DidCatchErrorBoundary, 'div'));
103
+
104
+ it('getDerivedStateFromError can recover by rendering an element of the same type', () =>
105
+ sharedTest(GetDerivedErrorBoundary, 'span'));
106
+
107
+ it('getDerivedStateFromError can recover by rendering an element of a different type', () =>
108
+ sharedTest(GetDerivedErrorBoundary, 'div'));
109
+ });
110
+ });
111
+});
packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js
+4
-1
@@ -2398,7 +2398,10 @@ describe('ReactIncremental', () => {
2398
instance.setState({
2399
throwError: true,
2400
});
2401
- ReactNoop.flush();
2401
+ expect(ReactNoop.flush).toWarnDev(
2402
+ 'Error boundaries should implement getDerivedStateFromError()',
2403
+ {withoutStack: true},
2404
+ );
2405
});
2406
2407
it('should not recreate masked context unless inputs have changed', () => {
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+95
-3
@@ -19,7 +19,6 @@ describe('ReactIncrementalErrorHandling', () => {
19
beforeEach(() => {
20
jest.resetModules();
21
ReactFeatureFlags = require('shared/ReactFeatureFlags');
22
- ReactFeatureFlags.enableGetDerivedStateFromCatch = true;
22
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
23
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
24
PropTypes = require('prop-types');
@@ -41,6 +40,99 @@ describe('ReactIncrementalErrorHandling', () => {
40
}
41
42
it('recovers from errors asynchronously', () => {
43
+ class ErrorBoundary extends React.Component {
44
+ state = {error: null};
45
+ static getDerivedStateFromError(error) {
46
+ ReactNoop.yield('getDerivedStateFromError');
47
+ return {error};
48
+ }
49
+ render() {
50
+ if (this.state.error) {
51
+ ReactNoop.yield('ErrorBoundary (catch)');
52
+ return <ErrorMessage error={this.state.error} />;
53
+ }
54
+ ReactNoop.yield('ErrorBoundary (try)');
55
+ return this.props.children;
56
+ }
57
+ }
58
+
59
+ function ErrorMessage(props) {
60
+ ReactNoop.yield('ErrorMessage');
61
+ return <span prop={`Caught an error: ${props.error.message}`} />;
62
+ }
63
+
64
+ function Indirection(props) {
65
+ ReactNoop.yield('Indirection');
66
+ return props.children || null;
67
+ }
68
+
69
+ function BadRender() {
70
+ ReactNoop.yield('throw');
71
+ throw new Error('oops!');
72
+ }
73
+
74
+ ReactNoop.render(
75
+ <ErrorBoundary>
76
+ <Indirection>
77
+ <Indirection>
78
+ <Indirection>
79
+ <BadRender />
80
+ <Indirection />
81
+ <Indirection />
82
+ </Indirection>
83
+ </Indirection>
84
+ </Indirection>
85
+ </ErrorBoundary>,
86
+ );
87
+
88
+ // Start rendering asynchronsouly
89
+ ReactNoop.flushThrough([
90
+ 'ErrorBoundary (try)',
91
+ 'Indirection',
92
+ 'Indirection',
93
+ 'Indirection',
94
+ // An error is thrown. React keeps rendering asynchronously.
95
+ 'throw',
96
+ ]);
97
+
98
+ // Still rendering async...
99
+ ReactNoop.flushThrough(['Indirection']);
100
+
101
+ ReactNoop.flushThrough([
102
+ 'Indirection',
103
+
104
+ // Call getDerivedStateFromError and re-render the error boundary, this
105
+ // time rendering an error message.
106
+ 'getDerivedStateFromError',
107
+ 'ErrorBoundary (catch)',
108
+ 'ErrorMessage',
109
+ ]);
110
+
111
+ // Since the error was thrown during an async render, React won't commit
112
+ // the result yet.
113
+ expect(ReactNoop.getChildren()).toEqual([]);
114
+
115
+ // Instead, it will try rendering one more time, synchronously, in case that
116
+ // happens to fix the error.
117
+ expect(ReactNoop.flushNextYield()).toEqual([
118
+ 'ErrorBoundary (try)',
119
+ 'Indirection',
120
+ 'Indirection',
121
+ 'Indirection',
122
+
123
+ // The error was thrown again. This time, React will actually commit
124
+ // the result.
125
+ 'throw',
126
+ 'Indirection',
127
+ 'Indirection',
128
+ 'getDerivedStateFromError',
129
+ 'ErrorBoundary (catch)',
130
+ 'ErrorMessage',
131
+ ]);
132
+ expect(ReactNoop.getChildren()).toEqual([span('Caught an error: oops!')]);
133
+ });
134
+
135
+ it('recovers from errors asynchronously (legacy, no getDerivedStateFromError)', () => {
136
class ErrorBoundary extends React.Component {
137
state = {error: null};
138
componentDidCatch(error) {
@@ -1442,10 +1534,10 @@ describe('ReactIncrementalErrorHandling', () => {
1534
]);
1535
});
1536
1445
- it('does not provide component stack to the error boundary with getDerivedStateFromCatch', () => {
1537
+ it('does not provide component stack to the error boundary with getDerivedStateFromError', () => {
1538
class ErrorBoundary extends React.Component {
1539
state = {error: null};
1448
- static getDerivedStateFromCatch(error, errorInfo) {
1540
+ static getDerivedStateFromError(error, errorInfo) {
1541
expect(errorInfo).toBeUndefined();
1542
return {error};
1543
}
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+1
-5
@@ -1316,11 +1316,7 @@ describe('ReactSuspense', () => {
1316
'A',
1317
'B',
1318
'C',
1319
- // 'A' matched with the placeholder. It's ok to reuse children when
1320
- // switching back. Though in a real app you probably don't want to.
1321
- // TODO: This is wrong. The timed out children and the placeholder
1322
- // should be siblings in async mode. Revisit in follow-up PR.
1323
- 'Update [A]',
1319
+ 'Mount [A]',
1320
'Mount [B]',
1321
'Mount [C]',
1322
]);
packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee
+3
-3
@@ -129,15 +129,15 @@ describe 'ReactCoffeeScriptClass', ->
129
).toWarnDev 'Foo: getDerivedStateFromProps() is defined as an instance method and will be ignored. Instead, declare it as a static method.', {withoutStack: true}
130
undefined
131
132
- it 'warns if getDerivedStateFromCatch is not static', ->
132
+ it 'warns if getDerivedStateFromError is not static', ->
133
class Foo extends React.Component
134
render: ->
135
div()
136
- getDerivedStateFromCatch: ->
136
+ getDerivedStateFromError: ->
137
{}
138
expect(->
139
ReactDOM.render(React.createElement(Foo, foo: 'foo'), container)
140
- ).toWarnDev 'Foo: getDerivedStateFromCatch() is defined as an instance method and will be ignored. Instead, declare it as a static method.', {withoutStack: true}
140
+ ).toWarnDev 'Foo: getDerivedStateFromError() is defined as an instance method and will be ignored. Instead, declare it as a static method.', {withoutStack: true}
141
undefined
142
143
it 'warns if getSnapshotBeforeUpdate is static', ->
packages/react/src/__tests__/ReactES6Class-test.js
+3
-3
@@ -147,9 +147,9 @@ describe('ReactES6Class', () => {
147
);
148
});
149
150
- it('warns if getDerivedStateFromCatch is not static', () => {
150
+ it('warns if getDerivedStateFromError is not static', () => {
151
class Foo extends React.Component {
152
- getDerivedStateFromCatch() {
152
+ getDerivedStateFromError() {
153
return {};
154
}
155
render() {
@@ -157,7 +157,7 @@ describe('ReactES6Class', () => {
157
}
158
}
159
expect(() => ReactDOM.render(<Foo foo="foo" />, container)).toWarnDev(
160
- 'Foo: getDerivedStateFromCatch() is defined as an instance method ' +
160
+ 'Foo: getDerivedStateFromError() is defined as an instance method ' +
161
'and will be ignored. Instead, declare it as a static method.',
162
{withoutStack: true},
163
);
packages/react/src/__tests__/ReactProfiler-test.internal.js
+2
-3
@@ -36,7 +36,6 @@ function loadModules({
36
ReactFeatureFlags.debugRenderPhaseSideEffects = false;
37
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
38
ReactFeatureFlags.enableProfilerTimer = enableProfilerTimer;
39
- ReactFeatureFlags.enableGetDerivedStateFromCatch = true;
39
ReactFeatureFlags.enableSchedulerTracing = enableSchedulerTracing;
40
ReactFeatureFlags.enableSuspense = enableSuspense;
41
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = replayFailedUnitOfWorkWithInvokeGuardedCallback;
@@ -985,7 +984,7 @@ describe('Profiler', () => {
984
);
985
});
986
988
- it('should accumulate actual time after an error handled by getDerivedStateFromCatch()', () => {
987
+ it('should accumulate actual time after an error handled by getDerivedStateFromError()', () => {
988
const callback = jest.fn();
989
990
const ThrowsError = () => {
@@ -995,7 +994,7 @@ describe('Profiler', () => {
994
995
class ErrorBoundary extends React.Component {
996
state = {error: null};
998
- static getDerivedStateFromCatch(error) {
997
+ static getDerivedStateFromError(error) {
998
return {error};
999
}
1000
render() {
packages/react/src/__tests__/ReactTypeScriptClass-test.ts
+3
-3
@@ -397,9 +397,9 @@ describe('ReactTypeScriptClass', function() {
397
);
398
});
399
400
- it('warns if getDerivedStateFromCatch is not static', function() {
400
+ it('warns if getDerivedStateFromError is not static', function() {
401
class Foo extends React.Component {
402
- getDerivedStateFromCatch() {
402
+ getDerivedStateFromError() {
403
return {};
404
}
405
render() {
@@ -409,7 +409,7 @@ describe('ReactTypeScriptClass', function() {
409
expect(function() {
410
ReactDOM.render(React.createElement(Foo, {foo: 'foo'}), container);
411
}).toWarnDev(
412
- 'Foo: getDerivedStateFromCatch() is defined as an instance method ' +
412
+ 'Foo: getDerivedStateFromError() is defined as an instance method ' +
413
'and will be ignored. Instead, declare it as a static method.',
414
{withoutStack: true}
415
);
packages/react/src/__tests__/createReactClassIntegration-test.js
+3
-3
@@ -459,9 +459,9 @@ describe('create-react-class-integration', () => {
459
);
460
});
461
462
- it('warns if getDerivedStateFromCatch is not static', () => {
462
+ it('warns if getDerivedStateFromError is not static', () => {
463
const Foo = createReactClass({
464
- getDerivedStateFromCatch() {
464
+ getDerivedStateFromError() {
465
return {};
466
},
467
render() {
@@ -471,7 +471,7 @@ describe('create-react-class-integration', () => {
471
expect(() =>
472
ReactDOM.render(<Foo foo="foo" />, document.createElement('div')),
473
).toWarnDev(
474
- 'Component: getDerivedStateFromCatch() is defined as an instance method ' +
474
+ 'Component: getDerivedStateFromError() is defined as an instance method ' +
475
'and will be ignored. Instead, declare it as a static method.',
476
{withoutStack: true},
477
);
packages/shared/ReactFeatureFlags.js
-3
@@ -10,9 +10,6 @@
10
// Exports ReactDOM.createRoot
11
export const enableUserTimingAPI = __DEV__;
12
13
-// Experimental error-boundary API that can recover from errors within a single
14
-// render phase
15
-export const enableGetDerivedStateFromCatch = false;
13
// Suspense
14
export const enableSuspense = false;
15
// Helps identify side effects in begin-phase lifecycle hooks and setState reducers:
packages/shared/forks/ReactFeatureFlags.native-fabric-fb.js
-1
@@ -15,7 +15,6 @@ import typeof * as FabricFeatureFlagsType from './ReactFeatureFlags.native-fabri
15
export const debugRenderPhaseSideEffects = false;
16
export const debugRenderPhaseSideEffectsForStrictMode = false;
17
export const enableUserTimingAPI = __DEV__;
18
-export const enableGetDerivedStateFromCatch = false;
18
export const enableSuspense = false;
19
export const warnAboutDeprecatedLifecycles = false;
20
export const warnAboutLegacyContextAPI = __DEV__;
packages/shared/forks/ReactFeatureFlags.native-fabric-oss.js
-1
@@ -15,7 +15,6 @@ import typeof * as FabricFeatureFlagsType from './ReactFeatureFlags.native-fabri
15
export const debugRenderPhaseSideEffects = false;
16
export const debugRenderPhaseSideEffectsForStrictMode = false;
17
export const enableUserTimingAPI = __DEV__;
18
-export const enableGetDerivedStateFromCatch = false;
18
export const enableSuspense = false;
19
export const warnAboutDeprecatedLifecycles = false;
20
export const warnAboutLegacyContextAPI = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
-1
@@ -14,7 +14,6 @@ import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.native-fb';
14
15
// Re-export dynamic flags from the fbsource version.
16
export const {
17
- enableGetDerivedStateFromCatch,
17
enableSuspense,
18
debugRenderPhaseSideEffects,
19
debugRenderPhaseSideEffectsForStrictMode,
packages/shared/forks/ReactFeatureFlags.native-oss.js
-1
@@ -14,7 +14,6 @@ import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.native-oss';
14
15
export const debugRenderPhaseSideEffects = false;
16
export const debugRenderPhaseSideEffectsForStrictMode = false;
17
-export const enableGetDerivedStateFromCatch = false;
17
export const enableSuspense = false;
18
export const enableUserTimingAPI = __DEV__;
19
export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__;
packages/shared/forks/ReactFeatureFlags.persistent.js
-1
@@ -15,7 +15,6 @@ import typeof * as PersistentFeatureFlagsType from './ReactFeatureFlags.persiste
15
export const debugRenderPhaseSideEffects = false;
16
export const debugRenderPhaseSideEffectsForStrictMode = false;
17
export const enableUserTimingAPI = __DEV__;
18
-export const enableGetDerivedStateFromCatch = false;
18
export const enableSuspense = false;
19
export const warnAboutDeprecatedLifecycles = false;
20
export const warnAboutLegacyContextAPI = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
-1
@@ -15,7 +15,6 @@ import typeof * as PersistentFeatureFlagsType from './ReactFeatureFlags.persiste
15
export const debugRenderPhaseSideEffects = false;
16
export const debugRenderPhaseSideEffectsForStrictMode = false;
17
export const enableUserTimingAPI = __DEV__;
18
-export const enableGetDerivedStateFromCatch = false;
18
export const enableSuspense = false;
19
export const warnAboutDeprecatedLifecycles = false;
20
export const warnAboutLegacyContextAPI = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
-1
@@ -15,7 +15,6 @@ import typeof * as PersistentFeatureFlagsType from './ReactFeatureFlags.persiste
15
export const debugRenderPhaseSideEffects = false;
16
export const debugRenderPhaseSideEffectsForStrictMode = false;
17
export const enableUserTimingAPI = __DEV__;
18
-export const enableGetDerivedStateFromCatch = false;
18
export const enableSuspense = true;
19
export const warnAboutDeprecatedLifecycles = false;
20
export const warnAboutLegacyContextAPI = false;
packages/shared/forks/ReactFeatureFlags.www.js
-1
@@ -15,7 +15,6 @@ export const {
15
enableSuspense,
16
debugRenderPhaseSideEffects,
17
debugRenderPhaseSideEffectsForStrictMode,
18
- enableGetDerivedStateFromCatch,
18
enableSuspenseServerRenderer,
19
replayFailedUnitOfWorkWithInvokeGuardedCallback,
20
warnAboutDeprecatedLifecycles,