@samitouri / QOS-React-2 / commits / 806eebdaee

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,