Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: facebook/react
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: e16d61c3000e2de6217d06b9afad162e883f73c4
Choose a base ref
...
head repository: facebook/react
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 40d62e9e0c1d992310597f4f6b672e5b6adb1a7a
Choose a head ref
  • 7 commits
  • 18 files changed
  • 2 contributors

Commits on Jun 2, 2021

  1. Copy the full SHA
    5d39e4f View commit details
  2. Copy the full SHA
    36827a4 View commit details
  3. Avoid eagerly detecting whether component is error boundary and separ…

    …ate the concepts between forced error states and console errors
    
    With this approach, we can get rid of the warning dialog
    baopham committed Jun 2, 2021
    Copy the full SHA
    958b787 View commit details
  4. Run prettier

    baopham committed Jun 2, 2021
    Copy the full SHA
    e3edb36 View commit details

Commits on Jun 3, 2021

  1. Copy the full SHA
    b78d210 View commit details
  2. Copy the full SHA
    094e874 View commit details
  3. Added a comment to kick off CI

    Brian Vaughn committed Jun 3, 2021
    Copy the full SHA
    40d62e9 View commit details
Original file line number Diff line number Diff line change
@@ -2380,4 +2380,95 @@ describe('InspectedElement', () => {
`);
});
});

describe('error boundary', () => {
it('can toggle error', async () => {
class ErrorBoundary extends React.Component<any> {
state = {hasError: false};
static getDerivedStateFromError(error) {
return {hasError: true};
}
render() {
const {hasError} = this.state;
return hasError ? 'has-error' : this.props.children;
}
}
const Example = () => 'example';

await utils.actAsync(() =>
ReactDOM.render(
<ErrorBoundary>
<Example />
</ErrorBoundary>,
document.createElement('div'),
),
);

const targetErrorBoundaryID = ((store.getElementIDAtIndex(
0,
): any): number);
const inspect = index => {
// HACK: Recreate TestRenderer instance so we can inspect different
// elements
testRendererInstance = TestRenderer.create(null, {
unstable_isConcurrent: true,
});
return inspectElementAtIndex(index);
};
const toggleError = async forceError => {
await withErrorsOrWarningsIgnored(['ErrorBoundary'], async () => {
await utils.actAsync(() => {
bridge.send('overrideError', {
id: targetErrorBoundaryID,
rendererID: store.getRendererIDForElement(targetErrorBoundaryID),
forceError,
});
});
});

TestUtilsAct(() => {
jest.runOnlyPendingTimers();
});
};

// Inspect <ErrorBoundary /> and see that we cannot toggle error state
// on error boundary itself
let inspectedElement = await inspect(0);
expect(inspectedElement.canToggleError).toBe(false);
expect(inspectedElement.targetErrorBoundaryID).toBe(null);

// Inspect <Example />
inspectedElement = await inspect(1);
expect(inspectedElement.canToggleError).toBe(true);
expect(inspectedElement.isErrored).toBe(false);
expect(inspectedElement.targetErrorBoundaryID).toBe(
targetErrorBoundaryID,
);

// now force error state on <Example />
await toggleError(true);

// we are in error state now, <Example /> won't show up
expect(store.getElementIDAtIndex(1)).toBe(null);

// Inpsect <ErrorBoundary /> to toggle off the error state
inspectedElement = await inspect(0);
expect(inspectedElement.canToggleError).toBe(true);
expect(inspectedElement.isErrored).toBe(true);
// its error boundary ID is itself because it's caught the error
expect(inspectedElement.targetErrorBoundaryID).toBe(
targetErrorBoundaryID,
);

await toggleError(false);

// We can now inspect <Example /> with ability to toggle again
inspectedElement = await inspect(1);
expect(inspectedElement.canToggleError).toBe(true);
expect(inspectedElement.isErrored).toBe(false);
expect(inspectedElement.targetErrorBoundaryID).toBe(
targetErrorBoundaryID,
);
});
});
});
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ export function test(maybeInspectedElement) {
hasOwnProperty('canEditFunctionProps') &&
hasOwnProperty('canEditHooks') &&
hasOwnProperty('canToggleSuspense') &&
hasOwnProperty('canToggleError') &&
hasOwnProperty('canViewSource')
);
}
16 changes: 16 additions & 0 deletions packages/react-devtools-shared/src/backend/agent.js
Original file line number Diff line number Diff line change
@@ -122,6 +122,12 @@ type OverrideValueAtPathParams = {|
value: any,
|};

type OverrideErrorParams = {|
id: number,
rendererID: number,
forceError: boolean,
|};

type OverrideSuspenseParams = {|
id: number,
rendererID: number,
@@ -183,6 +189,7 @@ export default class Agent extends EventEmitter<{|
bridge.addListener('getOwnersList', this.getOwnersList);
bridge.addListener('inspectElement', this.inspectElement);
bridge.addListener('logElementToConsole', this.logElementToConsole);
bridge.addListener('overrideError', this.overrideError);
bridge.addListener('overrideSuspense', this.overrideSuspense);
bridge.addListener('overrideValueAtPath', this.overrideValueAtPath);
bridge.addListener('reloadAndProfile', this.reloadAndProfile);
@@ -381,6 +388,15 @@ export default class Agent extends EventEmitter<{|
}
};

overrideError = ({id, rendererID, forceError}: OverrideErrorParams) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
} else {
renderer.overrideError(id, forceError);
}
};

overrideSuspense = ({
id,
rendererID,
9 changes: 9 additions & 0 deletions packages/react-devtools-shared/src/backend/legacy/renderer.js
Original file line number Diff line number Diff line change
@@ -800,6 +800,11 @@ export function attach(
canEditFunctionPropsDeletePaths: false,
canEditFunctionPropsRenamePaths: false,

// Toggle error boundary did not exist in legacy versions
canToggleError: false,
isErrored: false,
targetErrorBoundaryID: null,

// Suspense did not exist in legacy versions
canToggleSuspense: false,

@@ -1016,6 +1021,9 @@ export function attach(
const handlePostCommitFiberRoot = () => {
throw new Error('handlePostCommitFiberRoot not supported by this renderer');
};
const overrideError = () => {
throw new Error('overrideError not supported by this renderer');
};
const overrideSuspense = () => {
throw new Error('overrideSuspense not supported by this renderer');
};
@@ -1089,6 +1097,7 @@ export function attach(
handlePostCommitFiberRoot,
inspectElement,
logElementToConsole,
overrideError,
overrideSuspense,
overrideValueAtPath,
renamePath,
144 changes: 142 additions & 2 deletions packages/react-devtools-shared/src/backend/renderer.js
Original file line number Diff line number Diff line change
@@ -120,6 +120,7 @@ type ReactPriorityLevelsType = {|
|};

type ReactTypeOfSideEffectType = {|
DidCapture: number,
NoFlags: number,
PerformedWork: number,
Placement: number,
@@ -147,6 +148,7 @@ export function getInternalReactConstants(
ReactTypeOfWork: WorkTagMap,
|} {
const ReactTypeOfSideEffect: ReactTypeOfSideEffectType = {
DidCapture: 0b10000000,
NoFlags: 0b00,
PerformedWork: 0b01,
Placement: 0b10,
@@ -519,7 +521,13 @@ export function attach(
ReactTypeOfWork,
ReactTypeOfSideEffect,
} = getInternalReactConstants(version);
const {Incomplete, NoFlags, PerformedWork, Placement} = ReactTypeOfSideEffect;
const {
DidCapture,
Incomplete,
NoFlags,
PerformedWork,
Placement,
} = ReactTypeOfSideEffect;
const {
CacheComponent,
ClassComponent,
@@ -557,9 +565,13 @@ export function attach(
overrideProps,
overridePropsDeletePath,
overridePropsRenamePath,
setErrorHandler,
setSuspenseHandler,
scheduleUpdate,
} = renderer;
const supportsTogglingError =
typeof setErrorHandler === 'function' &&
typeof scheduleUpdate === 'function';
const supportsTogglingSuspense =
typeof setSuspenseHandler === 'function' &&
typeof scheduleUpdate === 'function';
@@ -659,6 +671,13 @@ export function attach(
type: 'error' | 'warn',
args: $ReadOnlyArray<any>,
): void {
if (type === 'error') {
const maybeID = getFiberIDUnsafe(fiber);
// if this is an error simulated by us to trigger error boundary, ignore
if (maybeID != null && forceErrorForFiberIDs.get(maybeID) === true) {
return;
}
}
const message = format(...args);
if (__DEBUG__) {
debug('onErrorOrWarning', fiber, null, `${type}: "${message}"`);
@@ -1133,6 +1152,13 @@ export function attach(
if (alternate !== null) {
fiberToIDMap.delete(alternate);
}

if (forceErrorForFiberIDs.has(fiberID)) {
forceErrorForFiberIDs.delete(fiberID);
if (forceErrorForFiberIDs.size === 0 && setErrorHandler != null) {
setErrorHandler(shouldErrorFiberAlwaysNull);
}
}
});
untrackFibersSet.clear();
}
@@ -2909,6 +2935,34 @@ export function attach(
return {instance, style};
}

function isErrorBoundary(fiber: Fiber): boolean {
const {tag, type} = fiber;

switch (tag) {
case ClassComponent:
case IncompleteClassComponent:
const instance = fiber.stateNode;
return (
typeof type.getDerivedStateFromError === 'function' ||
(instance !== null &&
typeof instance.componentDidCatch === 'function')
);
default:
return false;
}
}

function getNearestErrorBoundaryID(fiber: Fiber): number | null {
let parent = fiber.return;
while (parent !== null) {
if (isErrorBoundary(parent)) {
return getFiberIDUnsafe(parent);
}
parent = parent.return;
}
return null;
}

function inspectElementRaw(id: number): InspectedElement | null {
const fiber = findCurrentFiberUsingSlowPathById(id);
if (fiber == null) {
@@ -3063,6 +3117,21 @@ export function attach(
const errors = fiberIDToErrorsMap.get(id) || new Map();
const warnings = fiberIDToWarningsMap.get(id) || new Map();

const isErrored =
(fiber.flags & DidCapture) !== NoFlags ||
forceErrorForFiberIDs.get(id) === true;

let targetErrorBoundaryID;
if (isErrorBoundary(fiber)) {
// if the current inspected element is an error boundary,
// either that we want to use it to toggle off error state
// or that we allow to force error state on it if it's within another
// error boundary
targetErrorBoundaryID = isErrored ? id : getNearestErrorBoundaryID(fiber);
} else {
targetErrorBoundaryID = getNearestErrorBoundaryID(fiber);
}

return {
id,

@@ -3080,6 +3149,11 @@ export function attach(
canEditFunctionPropsRenamePaths:
typeof overridePropsRenamePath === 'function',

canToggleError: supportsTogglingError && targetErrorBoundaryID != null,
// Is this error boundary in error state.
isErrored,
targetErrorBoundaryID,

canToggleSuspense:
supportsTogglingSuspense &&
// If it's showing the real content, we can always flip fallback.
@@ -3747,7 +3821,72 @@ export function attach(
}

// React will switch between these implementations depending on whether
// we have any manually suspended Fibers or not.
// we have any manually suspended/errored-out Fibers or not.
function shouldErrorFiberAlwaysNull() {
return null;
}

// Map of id and its force error status: true (error), false (toggled off),
// null (do nothing)
const forceErrorForFiberIDs = new Map();
function shouldErrorFiberAccordingToMap(fiber) {
if (typeof setErrorHandler !== 'function') {
throw new Error(
'Expected overrideError() to not get called for earlier React versions.',
);
}

const id = getFiberIDUnsafe(fiber);
if (id === null) {
return null;
}

let status = null;
if (forceErrorForFiberIDs.has(id)) {
status = forceErrorForFiberIDs.get(id);
if (status === false) {
// TRICKY overrideError adds entries to this Map,
// so ideally it would be the method that clears them too,
// but that would break the functionality of the feature,
// since DevTools needs to tell React to act differently than it normally would
// (don't just re-render the failed boundary, but reset its errored state too).
// So we can only clear it after telling React to reset the state.
// Technically this is premature and we should schedule it for later,
// since the render could always fail without committing the updated error boundary,
// but since this is a DEV-only feature, the simplicity is worth the trade off.
forceErrorForFiberIDs.delete(id);

if (forceErrorForFiberIDs.size === 0) {
// Last override is gone. Switch React back to fast path.
setErrorHandler(shouldErrorFiberAlwaysNull);
}
}
}
return status;
}

function overrideError(id, forceError) {
if (
typeof setErrorHandler !== 'function' ||
typeof scheduleUpdate !== 'function'
) {
throw new Error(
'Expected overrideError() to not get called for earlier React versions.',
);
}

forceErrorForFiberIDs.set(id, forceError);

if (forceErrorForFiberIDs.size === 1) {
// First override is added. Switch React to slower path.
setErrorHandler(shouldErrorFiberAccordingToMap);
}

const fiber = idToArbitraryFiberMap.get(id);
if (fiber != null) {
scheduleUpdate(fiber);
}
}

function shouldSuspendFiberAlwaysFalse() {
return false;
@@ -4042,6 +4181,7 @@ export function attach(
logElementToConsole,
prepareViewAttributeSource,
prepareViewElementSource,
overrideError,
overrideSuspense,
overrideValueAtPath,
renamePath,
Loading