@@ -120,6 +120,7 @@ type ReactPriorityLevelsType = {|
120
120
| } ;
121
121
122
122
type ReactTypeOfSideEffectType = { |
123
+ DidCapture : number ,
123
124
NoFlags : number ,
124
125
PerformedWork : number ,
125
126
Placement : number ,
@@ -147,6 +148,7 @@ export function getInternalReactConstants(
147
148
ReactTypeOfWork : WorkTagMap ,
148
149
| } {
149
150
const ReactTypeOfSideEffect : ReactTypeOfSideEffectType = {
151
+ DidCapture : 0b10000000 ,
150
152
NoFlags : 0b00 ,
151
153
PerformedWork : 0b01 ,
152
154
Placement : 0b10 ,
@@ -519,7 +521,13 @@ export function attach(
519
521
ReactTypeOfWork,
520
522
ReactTypeOfSideEffect,
521
523
} = getInternalReactConstants ( version ) ;
522
- const { Incomplete, NoFlags, PerformedWork, Placement} = ReactTypeOfSideEffect ;
524
+ const {
525
+ DidCapture,
526
+ Incomplete,
527
+ NoFlags,
528
+ PerformedWork,
529
+ Placement,
530
+ } = ReactTypeOfSideEffect ;
523
531
const {
524
532
CacheComponent,
525
533
ClassComponent,
@@ -557,9 +565,13 @@ export function attach(
557
565
overrideProps,
558
566
overridePropsDeletePath,
559
567
overridePropsRenamePath,
568
+ setErrorHandler,
560
569
setSuspenseHandler,
561
570
scheduleUpdate,
562
571
} = renderer ;
572
+ const supportsTogglingError =
573
+ typeof setErrorHandler === 'function' &&
574
+ typeof scheduleUpdate === 'function' ;
563
575
const supportsTogglingSuspense =
564
576
typeof setSuspenseHandler === 'function' &&
565
577
typeof scheduleUpdate === 'function' ;
@@ -659,6 +671,13 @@ export function attach(
659
671
type : 'error' | 'warn' ,
660
672
args : $ReadOnlyArray < any > ,
661
673
) : 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
+ }
662
681
const message = format ( ...args ) ;
663
682
if ( __DEBUG__ ) {
664
683
debug ( 'onErrorOrWarning' , fiber , null , `${ type } : "${ message } "` ) ;
@@ -1133,6 +1152,13 @@ export function attach(
1133
1152
if (alternate !== null) {
1134
1153
fiberToIDMap . delete ( alternate ) ;
1135
1154
}
1155
+
1156
+ if (forceErrorForFiberIDs.has(fiberID)) {
1157
+ forceErrorForFiberIDs . delete ( fiberID ) ;
1158
+ if ( forceErrorForFiberIDs . size === 0 && setErrorHandler != null ) {
1159
+ setErrorHandler ( shouldErrorFiberAlwaysNull ) ;
1160
+ }
1161
+ }
1136
1162
} ) ;
1137
1163
untrackFibersSet . clear ( ) ;
1138
1164
}
@@ -2909,6 +2935,34 @@ export function attach(
2909
2935
return { instance, style} ;
2910
2936
}
2911
2937
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
+
2912
2966
function inspectElementRaw ( id : number ) : InspectedElement | null {
2913
2967
const fiber = findCurrentFiberUsingSlowPathById ( id ) ;
2914
2968
if ( fiber == null ) {
@@ -3063,6 +3117,21 @@ export function attach(
3063
3117
const errors = fiberIDToErrorsMap.get(id) || new Map();
3064
3118
const warnings = fiberIDToWarningsMap.get(id) || new Map();
3065
3119
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
+
3066
3135
return {
3067
3136
id ,
3068
3137
@@ -3080,6 +3149,11 @@ export function attach(
3080
3149
canEditFunctionPropsRenamePaths :
3081
3150
typeof overridePropsRenamePath === 'function' ,
3082
3151
3152
+ canToggleError : supportsTogglingError && targetErrorBoundaryID != null ,
3153
+ // Is this error boundary in error state.
3154
+ isErrored ,
3155
+ targetErrorBoundaryID ,
3156
+
3083
3157
canToggleSuspense :
3084
3158
supportsTogglingSuspense &&
3085
3159
// If it's showing the real content, we can always flip fallback.
@@ -3747,7 +3821,72 @@ export function attach(
3747
3821
}
3748
3822
3749
3823
// 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
+ }
3751
3890
3752
3891
function shouldSuspendFiberAlwaysFalse ( ) {
3753
3892
return false ;
@@ -4042,6 +4181,7 @@ export function attach(
4042
4181
logElementToConsole ,
4043
4182
prepareViewAttributeSource ,
4044
4183
prepareViewElementSource ,
4184
+ overrideError ,
4045
4185
overrideSuspense ,
4046
4186
overrideValueAtPath ,
4047
4187
renamePath ,
0 commit comments