Skip to content

Commit 352bf6d

Browse files
committed
[Fiber] Error boundaries
1 parent 0e23880 commit 352bf6d

File tree

4 files changed

+637
-197
lines changed

4 files changed

+637
-197
lines changed

src/renderers/shared/fiber/ReactFiberCommitWork.js

Lines changed: 86 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
'use strict';
1414

15+
import type { TrappedError } from 'ReactFiberErrorBoundary';
1516
import type { Fiber } from 'ReactFiber';
1617
import type { FiberRoot } from 'ReactFiberRoot';
1718
import type { HostConfig } from 'ReactFiberReconciler';
@@ -23,6 +24,7 @@ var {
2324
HostComponent,
2425
HostText,
2526
} = ReactTypeOfWork;
27+
var { trapError } = require('ReactFiberErrorBoundary');
2628
var { callCallbacks } = require('ReactFiberUpdateQueue');
2729

2830
var {
@@ -155,72 +157,94 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
155157
}
156158
}
157159

158-
function commitNestedUnmounts(root : Fiber) {
160+
function commitNestedUnmounts(root : Fiber): Array<TrappedError> | null {
161+
// Since errors are rare, we allocate this array on demand.
162+
let trappedErrors = null;
163+
159164
// While we're inside a removed host node we don't want to call
160165
// removeChild on the inner nodes because they're removed by the top
161166
// call anyway. We also want to call componentWillUnmount on all
162167
// composites before this host node is removed from the tree. Therefore
163168
// we do an inner loop while we're still inside the host node.
164169
let node : Fiber = root;
165170
while (true) {
166-
commitUnmount(node);
171+
const error = commitUnmount(node);
172+
if (error) {
173+
trappedErrors = trappedErrors || [];
174+
trappedErrors.push(error);
175+
}
167176
if (node.child) {
168177
// TODO: Coroutines need to visit the stateNode.
169178
node = node.child;
170179
continue;
171180
}
172181
if (node === root) {
173-
return;
182+
return trappedErrors;
174183
}
175184
while (!node.sibling) {
176185
if (!node.return || node.return === root) {
177-
return;
186+
return trappedErrors;
178187
}
179188
node = node.return;
180189
}
181190
node = node.sibling;
182191
}
192+
return trappedErrors;
183193
}
184194

185-
function unmountHostComponents(parent, current) {
195+
function unmountHostComponents(parent, current): Array<TrappedError> | null {
196+
// Since errors are rare, we allocate this array on demand.
197+
let trappedErrors = null;
198+
186199
// We only have the top Fiber that was inserted but we need recurse down its
187200
// children to find all the terminal nodes.
188201
let node : Fiber = current;
189202
while (true) {
190203
if (node.tag === HostComponent || node.tag === HostText) {
191-
commitNestedUnmounts(node);
204+
const errors = commitNestedUnmounts(node);
205+
if (errors) {
206+
if (!trappedErrors) {
207+
trappedErrors = errors;
208+
} else {
209+
trappedErrors.push.apply(trappedErrors, errors);
210+
}
211+
}
192212
// After all the children have unmounted, it is now safe to remove the
193213
// node from the tree.
194214
if (parent) {
195215
removeChild(parent, node.stateNode);
196216
}
197217
} else {
198-
commitUnmount(node);
218+
const error = commitUnmount(node);
219+
if (error) {
220+
trappedErrors = trappedErrors || [];
221+
trappedErrors.push(error);
222+
}
199223
if (node.child) {
200224
// TODO: Coroutines need to visit the stateNode.
201225
node = node.child;
202226
continue;
203227
}
204228
}
205229
if (node === current) {
206-
return;
230+
return trappedErrors;
207231
}
208232
while (!node.sibling) {
209233
if (!node.return || node.return === current) {
210-
return;
234+
return trappedErrors;
211235
}
212236
node = node.return;
213237
}
214238
node = node.sibling;
215239
}
240+
return trappedErrors;
216241
}
217242

218-
function commitDeletion(current : Fiber) : void {
243+
function commitDeletion(current : Fiber) : Array<TrappedError> | null {
219244
// Recursively delete all host nodes from the parent.
220-
// TODO: Error handling.
221245
const parent = getHostParent(current);
222-
223-
unmountHostComponents(parent, current);
246+
// Detach refs and call componentWillUnmount() on the whole subtree.
247+
const trappedErrors = unmountHostComponents(parent, current);
224248

225249
// Cut off the return pointers to disconnect it from the tree. Ideally, we
226250
// should clear the child pointer of the parent alternate to let this
@@ -233,21 +257,29 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
233257
current.alternate.child = null;
234258
current.alternate.return = null;
235259
}
260+
261+
return trappedErrors;
236262
}
237263

238-
function commitUnmount(current : Fiber) : void {
264+
function commitUnmount(current : Fiber) : TrappedError | null {
239265
switch (current.tag) {
240266
case ClassComponent: {
241267
detachRef(current);
242268
const instance = current.stateNode;
243269
if (typeof instance.componentWillUnmount === 'function') {
244-
instance.componentWillUnmount();
270+
const error = tryCallComponentWillUnmount(instance);
271+
if (error) {
272+
return trapError(current, error);
273+
}
245274
}
246-
return;
275+
return null;
247276
}
248277
case HostComponent: {
249278
detachRef(current);
250-
return;
279+
return null;
280+
}
281+
default: {
282+
return null;
251283
}
252284
}
253285
}
@@ -292,19 +324,20 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
292324
}
293325
}
294326

295-
function commitLifeCycles(current : ?Fiber, finishedWork : Fiber) : void {
327+
function commitLifeCycles(current : ?Fiber, finishedWork : Fiber) : TrappedError | null {
296328
switch (finishedWork.tag) {
297329
case ClassComponent: {
298330
const instance = finishedWork.stateNode;
331+
let error = null;
299332
if (!current) {
300333
if (typeof instance.componentDidMount === 'function') {
301-
instance.componentDidMount();
334+
error = tryCallComponentDidMount(instance);
302335
}
303336
} else {
304337
if (typeof instance.componentDidUpdate === 'function') {
305338
const prevProps = current.memoizedProps;
306339
const prevState = current.memoizedState;
307-
instance.componentDidUpdate(prevProps, prevState);
340+
error = tryCallComponentDidUpdate(instance, prevProps, prevState);
308341
}
309342
}
310343
// Clear updates from current fiber. This must go before the callbacks
@@ -320,7 +353,10 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
320353
callCallbacks(callbackList, instance);
321354
}
322355
attachRef(current, finishedWork, instance);
323-
return;
356+
if (error) {
357+
return trapError(finishedWork, error);
358+
}
359+
return null;
324360
}
325361
case HostContainer: {
326362
const instance = finishedWork.stateNode;
@@ -333,17 +369,44 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
333369
case HostComponent: {
334370
const instance : I = finishedWork.stateNode;
335371
attachRef(current, finishedWork, instance);
336-
return;
372+
return null;
337373
}
338374
case HostText: {
339375
// We have no life-cycles associated with text.
340-
return;
376+
return null;
341377
}
342378
default:
343379
throw new Error('This unit of work tag should not have side-effects.');
344380
}
345381
}
346382

383+
function tryCallComponentDidMount(instance) {
384+
try {
385+
instance.componentDidMount();
386+
return null;
387+
} catch (error) {
388+
return error;
389+
}
390+
}
391+
392+
function tryCallComponentDidUpdate(instance, prevProps, prevState) {
393+
try {
394+
instance.componentDidUpdate(prevProps, prevState);
395+
return null;
396+
} catch (error) {
397+
return error;
398+
}
399+
}
400+
401+
function tryCallComponentWillUnmount(instance) {
402+
try {
403+
instance.componentWillUnmount();
404+
return null;
405+
} catch (error) {
406+
return error;
407+
}
408+
}
409+
347410
return {
348411
commitInsertion,
349412
commitDeletion,
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Copyright 2013-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @providesModule ReactFiberErrorBoundary
10+
* @flow
11+
*/
12+
13+
'use strict';
14+
15+
import type { Fiber } from 'ReactFiber';
16+
17+
var {
18+
ClassComponent,
19+
} = require('ReactTypeOfWork');
20+
21+
export type TrappedError = {
22+
boundary: Fiber | null,
23+
error: any,
24+
};
25+
26+
function findClosestErrorBoundary(fiber : Fiber): Fiber | null {
27+
let maybeErrorBoundary = fiber.return;
28+
while (maybeErrorBoundary) {
29+
if (maybeErrorBoundary.tag === ClassComponent) {
30+
const instance = maybeErrorBoundary.stateNode;
31+
if (typeof instance.unstable_handleError === 'function') {
32+
return maybeErrorBoundary;
33+
}
34+
}
35+
maybeErrorBoundary = maybeErrorBoundary.return;
36+
}
37+
return null;
38+
}
39+
40+
function trapError(fiber : Fiber, error : any) : TrappedError {
41+
return {
42+
boundary: findClosestErrorBoundary(fiber),
43+
error,
44+
};
45+
}
46+
47+
function acknowledgeErrorInBoundary(boundary : Fiber, error : any) {
48+
const instance = boundary.stateNode;
49+
instance.unstable_handleError(error);
50+
}
51+
52+
exports.trapError = trapError;
53+
exports.acknowledgeErrorInBoundary = acknowledgeErrorInBoundary;

0 commit comments

Comments
 (0)