Skip to content

Commit 833a7f8

Browse files
committed
[Fiber] Initial error boundaries
1 parent 08da843 commit 833a7f8

File tree

6 files changed

+144
-43
lines changed

6 files changed

+144
-43
lines changed

src/renderers/shared/fiber/ReactFiber.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ export type Fiber = Instance & {
129129
progressedFirstDeletion: ?Fiber,
130130
progressedLastDeletion: ?Fiber,
131131

132+
// This flag gets set to true for error boundaries when they attempt to
133+
// render the error path. This way we know that if rendering fails, we should
134+
// skip this error boundary as failed and propagate the error above.
135+
hasErrored: boolean,
136+
132137
// This is a pooled version of a Fiber. Every fiber that gets updated will
133138
// eventually have a pair. There are cases when we can clean up pairs to save
134139
// memory if we need to.
@@ -194,6 +199,8 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {
194199
progressedFirstDeletion: null,
195200
progressedLastDeletion: null,
196201

202+
hasErrored: false,
203+
197204
alternate: null,
198205

199206
};
@@ -251,6 +258,7 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
251258
alt.updateQueue = fiber.updateQueue;
252259
alt.callbackList = fiber.callbackList;
253260
alt.pendingWorkPriority = priorityLevel;
261+
alt.hasErrored = fiber.hasErrored;
254262

255263
alt.memoizedProps = fiber.memoizedProps;
256264
alt.memoizedState = fiber.memoizedState;
@@ -327,4 +335,3 @@ exports.createFiberFromYield = function(yieldNode : ReactYield, priorityLevel :
327335
fiber.pendingProps = {};
328336
return fiber;
329337
};
330-

src/renderers/shared/fiber/ReactFiberBeginWork.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,12 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>, s
175175
var ctor = workInProgress.type;
176176
workInProgress.stateNode = instance = new ctor(props);
177177
mount(workInProgress, instance);
178-
state = instance.state || null;
178+
updateQueue = workInProgress.updateQueue;
179+
if (instance.state) {
180+
state = mergeUpdateQueue(updateQueue, instance.state, props);
181+
} else {
182+
state = null;
183+
}
179184
} else if (typeof instance.shouldComponentUpdate === 'function' &&
180185
!(updateQueue && updateQueue.isForced)) {
181186
if (workInProgress.memoizedProps !== null) {

src/renderers/shared/fiber/ReactFiberClassComponent.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : Priori
8080
// The instance needs access to the fiber so that it can schedule updates
8181
ReactInstanceMap.set(instance, workInProgress);
8282
instance.updater = updater;
83+
84+
if (typeof instance.componentWillMount === 'function') {
85+
instance.componentWillMount();
86+
}
8387
}
8488

8589
return {

src/renderers/shared/fiber/ReactFiberCompleteWork.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
129129
// Transfer update queue to callbackList field so callbacks can be
130130
// called during commit phase.
131131
workInProgress.callbackList = workInProgress.updateQueue;
132+
// Reset the flag tracking whether an error boundary has failed.
133+
workInProgress.hasErrored = false;
132134
markUpdate(workInProgress);
133135
return null;
134136
case HostContainer:

src/renderers/shared/fiber/ReactFiberScheduler.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ var {
3939
} = require('ReactTypeOfSideEffect');
4040

4141
var {
42+
ClassComponent,
4243
HostContainer,
4344
} = require('ReactTypeOfWork');
4445

@@ -268,13 +269,49 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
268269
}
269270
}
270271

272+
function handleError(workInProgress, error) {
273+
var parent = workInProgress;
274+
while (parent = parent.return) {
275+
if (parent.tag !== ClassComponent) {
276+
continue;
277+
}
278+
if (parent.hasErrored) {
279+
continue;
280+
}
281+
var instance = parent.stateNode;
282+
if (!instance.unstable_handleError) {
283+
continue;
284+
}
285+
286+
var boundary = parent;
287+
boundary.hasErrored = true;
288+
boundary.child = null;
289+
boundary.effectTag = NoEffect;
290+
boundary.nextEffect = null;
291+
boundary.firstEffect = null;
292+
boundary.lastEffect = null;
293+
boundary.progressedPriority = NoWork;
294+
boundary.progressedChild = null;
295+
boundary.progressedFirstDeletion = null;
296+
boundary.progressedLastDeletion = null;
297+
instance.unstable_handleError(error);
298+
return boundary;
299+
}
300+
throw error;
301+
}
302+
271303
function performUnitOfWork(workInProgress : Fiber) : ?Fiber {
272304
// The current, flushed, state of this fiber is the alternate.
273305
// Ideally nothing should rely on this, but relying on it here
274306
// means that we don't need an additional field on the work in
275307
// progress.
276308
const current = workInProgress.alternate;
277-
const next = beginWork(current, workInProgress, nextPriorityLevel);
309+
let next = null;
310+
try {
311+
next = beginWork(current, workInProgress, nextPriorityLevel);
312+
} catch (err) {
313+
next = handleError(workInProgress, err);
314+
}
278315

279316
if (next) {
280317
// If this spawns new work, do that next.

src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js

Lines changed: 86 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
'use strict';
1313

14+
var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
15+
1416
var React;
1517
var ReactDOM;
1618

@@ -33,6 +35,9 @@ describe('ReactErrorBoundaries', () => {
3335
var Normal;
3436

3537
beforeEach(() => {
38+
// TODO: Fiber isn't error resilient and one test can bring down them all.
39+
jest.resetModuleRegistry();
40+
3641
ReactDOM = require('ReactDOM');
3742
React = require('React');
3843

@@ -459,52 +464,93 @@ describe('ReactErrorBoundaries', () => {
459464
};
460465
});
461466

462-
// Known limitation: error boundary only "sees" errors caused by updates
463-
// flowing through it. This might be easier to fix in Fiber.
464-
it('currently does not catch errors originating downstream', () => {
465-
var fail = false;
466-
class Stateful extends React.Component {
467-
state = {shouldThrow: false};
467+
if (ReactDOMFeatureFlags.useFiber) {
468+
// This test implements a new feature in Fiber.
469+
it('catches errors originating downstream', () => {
470+
var fail = false;
471+
class Stateful extends React.Component {
472+
state = {shouldThrow: false};
473+
474+
render() {
475+
if (fail) {
476+
log.push('Stateful render [!]');
477+
throw new Error('Hello');
478+
}
479+
return <div />;
480+
}
481+
}
468482

469-
render() {
470-
if (fail) {
471-
log.push('Stateful render [!]');
472-
throw new Error('Hello');
483+
var statefulInst;
484+
var container = document.createElement('div');
485+
ReactDOM.render(
486+
<ErrorBoundary>
487+
<Stateful ref={inst => statefulInst = inst} />
488+
</ErrorBoundary>,
489+
container
490+
);
491+
492+
log.length = 0;
493+
expect(() => {
494+
fail = true;
495+
statefulInst.forceUpdate();
496+
}).not.toThrow();
497+
498+
expect(log).toEqual([
499+
'Stateful render [!]',
500+
'ErrorBoundary unstable_handleError',
501+
'ErrorBoundary render error',
502+
'ErrorBoundary componentDidUpdate',
503+
]);
504+
505+
log.length = 0;
506+
ReactDOM.unmountComponentAtNode(container);
507+
expect(log).toEqual([
508+
'ErrorBoundary componentWillUnmount',
509+
]);
510+
});
511+
} else {
512+
// Known limitation: error boundary only "sees" errors caused by updates
513+
// flowing through it. This is fixed in Fiber.
514+
it('currently does not catch errors originating downstream', () => {
515+
var fail = false;
516+
class Stateful extends React.Component {
517+
state = {shouldThrow: false};
518+
519+
render() {
520+
if (fail) {
521+
log.push('Stateful render [!]');
522+
throw new Error('Hello');
523+
}
524+
return <div />;
473525
}
474-
return <div />;
475526
}
476-
}
477527

478-
var statefulInst;
479-
var container = document.createElement('div');
480-
ReactDOM.render(
481-
<ErrorBoundary>
482-
<Stateful ref={inst => statefulInst = inst} />
483-
</ErrorBoundary>,
484-
container
485-
);
528+
var statefulInst;
529+
var container = document.createElement('div');
530+
ReactDOM.render(
531+
<ErrorBoundary>
532+
<Stateful ref={inst => statefulInst = inst} />
533+
</ErrorBoundary>,
534+
container
535+
);
486536

487-
log.length = 0;
488-
expect(() => {
489-
fail = true;
490-
statefulInst.forceUpdate();
491-
}).toThrow();
537+
log.length = 0;
538+
expect(() => {
539+
fail = true;
540+
statefulInst.forceUpdate();
541+
}).toThrow();
492542

493-
expect(log).toEqual([
494-
'Stateful render [!]',
495-
// FIXME: uncomment when downstream errors get caught.
496-
// Catch and render an error message
497-
// 'ErrorBoundary unstable_handleError',
498-
// 'ErrorBoundary render error',
499-
// 'ErrorBoundary componentDidUpdate',
500-
]);
543+
expect(log).toEqual([
544+
'Stateful render [!]',
545+
]);
501546

502-
log.length = 0;
503-
ReactDOM.unmountComponentAtNode(container);
504-
expect(log).toEqual([
505-
'ErrorBoundary componentWillUnmount',
506-
]);
507-
});
547+
log.length = 0;
548+
ReactDOM.unmountComponentAtNode(container);
549+
expect(log).toEqual([
550+
'ErrorBoundary componentWillUnmount',
551+
]);
552+
});
553+
}
508554

509555
it('renders an error state if child throws in render', () => {
510556
var container = document.createElement('div');
@@ -860,7 +906,7 @@ describe('ReactErrorBoundaries', () => {
860906
'BrokenRender render [!]',
861907
// Handle error:
862908
'ErrorBoundary unstable_handleError',
863-
// Child ref wasn't (and won't be) set but there's no harm in clearing:
909+
// TODO: This is unnecessary, and Fiber doesn't do it:
864910
'Child ref is set to null',
865911
'ErrorBoundary render error',
866912
// Ref to error message should get set:

0 commit comments

Comments
 (0)