@@ -120,6 +120,7 @@ type ReactPriorityLevelsType = {|
120120| } ;
121121
122122type ReactTypeOfSideEffectType = { |
123+ DidCapture : number ,
123124 NoFlags : number ,
124125 PerformedWork : number ,
125126 Placement : number ,
@@ -147,6 +148,7 @@ export function getInternalReactConstants(
147148 ReactTypeOfWork : WorkTagMap ,
148149| } {
149150 const ReactTypeOfSideEffect : ReactTypeOfSideEffectType = {
151+ DidCapture : 0b10000000 ,
150152 NoFlags : 0b00 ,
151153 PerformedWork : 0b01 ,
152154 Placement : 0b10 ,
@@ -519,7 +521,13 @@ export function attach(
519521 ReactTypeOfWork,
520522 ReactTypeOfSideEffect,
521523 } = getInternalReactConstants ( version ) ;
522- const { Incomplete, NoFlags, PerformedWork, Placement} = ReactTypeOfSideEffect ;
524+ const {
525+ DidCapture,
526+ Incomplete,
527+ NoFlags,
528+ PerformedWork,
529+ Placement,
530+ } = ReactTypeOfSideEffect ;
523531 const {
524532 CacheComponent,
525533 ClassComponent,
@@ -557,9 +565,13 @@ export function attach(
557565 overrideProps,
558566 overridePropsDeletePath,
559567 overridePropsRenamePath,
568+ setErrorHandler,
560569 setSuspenseHandler,
561570 scheduleUpdate,
562571 } = renderer ;
572+ const supportsTogglingError =
573+ typeof setErrorHandler === 'function' &&
574+ typeof scheduleUpdate === 'function' ;
563575 const supportsTogglingSuspense =
564576 typeof setSuspenseHandler === 'function' &&
565577 typeof scheduleUpdate === 'function' ;
@@ -659,6 +671,13 @@ export function attach(
659671 type : 'error' | 'warn' ,
660672 args : $ReadOnlyArray < any > ,
661673 ) : void {
674+ if ( type === 'error' ) {
675+ const maybeID = getFiberIDUnsafe ( fiber ) ;
676+ // if this is an error simulated by us to trigger error boundary, ignore
677+ if ( maybeID != null && forceErrorForFiberIDs . get ( maybeID ) === true ) {
678+ return ;
679+ }
680+ }
662681 const message = format ( ...args ) ;
663682 if ( __DEBUG__ ) {
664683 debug ( 'onErrorOrWarning' , fiber , null , `${ type } : "${ message } "` ) ;
@@ -1133,6 +1152,13 @@ export function attach(
11331152 if (alternate !== null) {
11341153 fiberToIDMap . delete ( alternate ) ;
11351154 }
1155+
1156+ if (forceErrorForFiberIDs.has(fiberID)) {
1157+ forceErrorForFiberIDs . delete ( fiberID ) ;
1158+ if ( forceErrorForFiberIDs . size === 0 && setErrorHandler != null ) {
1159+ setErrorHandler ( shouldErrorFiberAlwaysNull ) ;
1160+ }
1161+ }
11361162 } ) ;
11371163 untrackFibersSet . clear ( ) ;
11381164 }
@@ -2909,6 +2935,34 @@ export function attach(
29092935 return { instance, style} ;
29102936 }
29112937
2938+ function isErrorBoundary ( fiber : Fiber ) : boolean {
2939+ const { tag, type} = fiber ;
2940+
2941+ switch ( tag ) {
2942+ case ClassComponent :
2943+ case IncompleteClassComponent :
2944+ const instance = fiber . stateNode ;
2945+ return (
2946+ typeof type . getDerivedStateFromError === 'function' ||
2947+ ( instance !== null &&
2948+ typeof instance . componentDidCatch === 'function' )
2949+ ) ;
2950+ default :
2951+ return false ;
2952+ }
2953+ }
2954+
2955+ function getNearestErrorBoundaryID ( fiber : Fiber ) : number | null {
2956+ let parent = fiber . return ;
2957+ while ( parent !== null ) {
2958+ if ( isErrorBoundary ( parent ) ) {
2959+ return getFiberIDUnsafe ( parent ) ;
2960+ }
2961+ parent = parent . return ;
2962+ }
2963+ return null ;
2964+ }
2965+
29122966 function inspectElementRaw ( id : number ) : InspectedElement | null {
29132967 const fiber = findCurrentFiberUsingSlowPathById ( id ) ;
29142968 if ( fiber == null ) {
@@ -3063,6 +3117,21 @@ export function attach(
30633117 const errors = fiberIDToErrorsMap.get(id) || new Map();
30643118 const warnings = fiberIDToWarningsMap.get(id) || new Map();
30653119
3120+ const isErrored =
3121+ (fiber.flags & DidCapture ) !== NoFlags ||
3122+ forceErrorForFiberIDs . get ( id ) === true ;
3123+
3124+ let targetErrorBoundaryID ;
3125+ if ( isErrorBoundary ( fiber ) ) {
3126+ // if the current inspected element is an error boundary,
3127+ // either that we want to use it to toggle off error state
3128+ // or that we allow to force error state on it if it's within another
3129+ // error boundary
3130+ targetErrorBoundaryID = isErrored ? id : getNearestErrorBoundaryID ( fiber ) ;
3131+ } else {
3132+ targetErrorBoundaryID = getNearestErrorBoundaryID ( fiber ) ;
3133+ }
3134+
30663135 return {
30673136 id ,
30683137
@@ -3080,6 +3149,11 @@ export function attach(
30803149 canEditFunctionPropsRenamePaths :
30813150 typeof overridePropsRenamePath === 'function' ,
30823151
3152+ canToggleError : supportsTogglingError && targetErrorBoundaryID != null ,
3153+ // Is this error boundary in error state.
3154+ isErrored ,
3155+ targetErrorBoundaryID ,
3156+
30833157 canToggleSuspense :
30843158 supportsTogglingSuspense &&
30853159 // If it's showing the real content, we can always flip fallback.
@@ -3747,7 +3821,72 @@ export function attach(
37473821 }
37483822
37493823 // React will switch between these implementations depending on whether
3750- // we have any manually suspended Fibers or not.
3824+ // we have any manually suspended/errored-out Fibers or not.
3825+ function shouldErrorFiberAlwaysNull() {
3826+ return null ;
3827+ }
3828+
3829+ // Map of id and its force error status: true (error), false (toggled off),
3830+ // null (do nothing)
3831+ const forceErrorForFiberIDs = new Map();
3832+ function shouldErrorFiberAccordingToMap(fiber) {
3833+ if ( typeof setErrorHandler !== 'function' ) {
3834+ throw new Error (
3835+ 'Expected overrideError() to not get called for earlier React versions.' ,
3836+ ) ;
3837+ }
3838+
3839+ const id = getFiberIDUnsafe(fiber);
3840+ if (id === null) {
3841+ return null ;
3842+ }
3843+
3844+ let status = null;
3845+ if (forceErrorForFiberIDs.has(id)) {
3846+ status = forceErrorForFiberIDs . get ( id ) ;
3847+ if ( status === false ) {
3848+ // TRICKY overrideError adds entries to this Map,
3849+ // so ideally it would be the method that clears them too,
3850+ // but that would break the functionality of the feature,
3851+ // since DevTools needs to tell React to act differently than it normally would
3852+ // (don't just re-render the failed boundary, but reset its errored state too).
3853+ // So we can only clear it after telling React to reset the state.
3854+ // Technically this is premature and we should schedule it for later,
3855+ // since the render could always fail without committing the updated error boundary,
3856+ // but since this is a DEV-only feature, the simplicity is worth the trade off.
3857+ forceErrorForFiberIDs . delete ( id ) ;
3858+
3859+ if ( forceErrorForFiberIDs . size === 0 ) {
3860+ // Last override is gone. Switch React back to fast path.
3861+ setErrorHandler ( shouldErrorFiberAlwaysNull ) ;
3862+ }
3863+ }
3864+ }
3865+ return status ;
3866+ }
3867+
3868+ function overrideError ( id , forceError ) {
3869+ if (
3870+ typeof setErrorHandler !== 'function' ||
3871+ typeof scheduleUpdate !== 'function'
3872+ ) {
3873+ throw new Error (
3874+ 'Expected overrideError() to not get called for earlier React versions.' ,
3875+ ) ;
3876+ }
3877+
3878+ forceErrorForFiberIDs.set(id, forceError);
3879+
3880+ if (forceErrorForFiberIDs.size === 1) {
3881+ // First override is added. Switch React to slower path.
3882+ setErrorHandler ( shouldErrorFiberAccordingToMap ) ;
3883+ }
3884+
3885+ const fiber = idToArbitraryFiberMap.get(id);
3886+ if (fiber != null) {
3887+ scheduleUpdate ( fiber ) ;
3888+ }
3889+ }
37513890
37523891 function shouldSuspendFiberAlwaysFalse ( ) {
37533892 return false ;
@@ -4042,6 +4181,7 @@ export function attach(
40424181 logElementToConsole ,
40434182 prepareViewAttributeSource ,
40444183 prepareViewElementSource ,
4184+ overrideError ,
40454185 overrideSuspense ,
40464186 overrideValueAtPath ,
40474187 renamePath ,
0 commit comments