-
Notifications
You must be signed in to change notification settings - Fork 47.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Fiber] Initial error boundaries #7993
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,6 +40,7 @@ var { | |
|
||
var { | ||
HostContainer, | ||
ClassComponent, | ||
} = require('ReactTypeOfWork'); | ||
|
||
var timeHeuristicForUnitOfWork = 1; | ||
|
@@ -285,7 +286,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) { | |
} | ||
} | ||
|
||
function performDeferredWork(deadline) { | ||
function performDeferredWorkUnsafe(deadline) { | ||
if (!nextUnitOfWork) { | ||
nextUnitOfWork = findNextUnitOfWork(); | ||
} | ||
|
@@ -303,6 +304,23 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) { | |
} | ||
} | ||
|
||
function performDeferredWork(deadline) { | ||
try { | ||
performDeferredWorkUnsafe(deadline); | ||
} catch (error) { | ||
const failedUnitOfWork = nextUnitOfWork; | ||
// Reset because it points to the error boundary: | ||
nextUnitOfWork = null; | ||
if (failedUnitOfWork) { | ||
handleError(failedUnitOfWork, error); | ||
} else { | ||
// We shouldn't end up here because nextUnitOfWork | ||
// should always be set while work is being performed. | ||
throw error; | ||
} | ||
} | ||
} | ||
|
||
function scheduleDeferredWork(root : FiberRoot, priority : PriorityLevel) { | ||
// We must reset the current unit of work pointer so that we restart the | ||
// search from the root during the next tick, in case there is now higher | ||
|
@@ -334,7 +352,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) { | |
} | ||
} | ||
|
||
function performAnimationWork() { | ||
function performAnimationWorkUnsafe() { | ||
// Always start from the root | ||
nextUnitOfWork = findNextUnitOfWork(); | ||
while (nextUnitOfWork && | ||
|
@@ -352,6 +370,23 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) { | |
} | ||
} | ||
|
||
function performAnimationWork() { | ||
try { | ||
performAnimationWorkUnsafe(); | ||
} catch (error) { | ||
const failedUnitOfWork = nextUnitOfWork; | ||
// Reset because it points to the error boundary: | ||
nextUnitOfWork = null; | ||
if (failedUnitOfWork) { | ||
handleError(failedUnitOfWork, error); | ||
} else { | ||
// We shouldn't end up here because nextUnitOfWork | ||
// should always be set while work is being performed. | ||
throw error; | ||
} | ||
} | ||
} | ||
|
||
function scheduleAnimationWork(root: FiberRoot, priorityLevel : PriorityLevel) { | ||
// Set the priority on the root, without deprioritizing | ||
if (root.current.pendingWorkPriority === NoWork || | ||
|
@@ -426,6 +461,60 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) { | |
} | ||
} | ||
|
||
function findClosestErrorBoundary(fiber : Fiber): ?Fiber { | ||
let maybeErrorBoundary = fiber.return; | ||
while (maybeErrorBoundary) { | ||
if (maybeErrorBoundary.tag === ClassComponent) { | ||
const instance = maybeErrorBoundary.stateNode; | ||
if (typeof instance.unstable_handleError === 'function') { | ||
return maybeErrorBoundary; | ||
} | ||
} | ||
maybeErrorBoundary = maybeErrorBoundary.return; | ||
} | ||
return null; | ||
} | ||
|
||
function handleError(failedUnitOfWork : Fiber, error : any) { | ||
const errorBoundary = findClosestErrorBoundary(failedUnitOfWork); | ||
if (errorBoundary) { | ||
handleErrorInBoundary(errorBoundary, error); | ||
return; | ||
} | ||
// TODO: Do we need to reset nextUnitOfWork here? | ||
throw error; | ||
} | ||
|
||
function handleErrorInBoundary(errorBoundary : Fiber, error : any) { | ||
// The work below failed so we need to clear it out and try to render the error state. | ||
// TODO: Do we need to clear all of these fields? How do we teardown an existing tree? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we naively just clear everything here, we'll lose the ability to unmount them. I don't think we need to clear anything here. We normally we reset everything to whatever the "current" tree represents on the way down the tree. The simplest way might be to restart from the root. In theory the error boundary will already be a clone, it might be fine to just restart from it. However, |
||
errorBoundary.child = null; | ||
errorBoundary.effectTag = NoEffect; | ||
errorBoundary.nextEffect = null; | ||
errorBoundary.firstEffect = null; | ||
errorBoundary.lastEffect = null; | ||
errorBoundary.progressedPriority = NoWork; | ||
errorBoundary.progressedChild = null; | ||
errorBoundary.progressedFirstDeletion = null; | ||
errorBoundary.progressedLastDeletion = null; | ||
|
||
// Error boundary implementations would usually call setState() here: | ||
const instance = errorBoundary.stateNode; | ||
instance.unstable_handleError(error); | ||
|
||
try { | ||
// The error path should be processed synchronously. | ||
// This lets us easily propagate errors to a parent boundary. | ||
let unitOfWork = errorBoundary; | ||
while (unitOfWork) { | ||
unitOfWork = performUnitOfWork(unitOfWork); | ||
} | ||
} catch (nextError) { | ||
// Propagate error to the next boundary or rethrow. | ||
handleError(errorBoundary, nextError); | ||
} | ||
} | ||
|
||
return { | ||
scheduleWork: scheduleWork, | ||
scheduleDeferredWork: scheduleDeferredWork, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is always safe to set nextUnitOfWork to
null
because it will just find its place again.