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
Add support for componentDidCatch Component method #819
Changes from 31 commits
d54d337
6ff8cc6
721b437
dde11b2
e105500
3705d4e
e8776c7
980b4cb
89efb5e
b671425
31f5fd9
3a3d7cb
313ffe6
d4612f4
1f164dd
b35c534
aa48769
a2b4c8c
4320ff9
4338a7c
74fddde
65ff737
8b23777
308d8aa
b853143
30cde7b
b98159a
286e6ea
be4dee5
a019562
3a2a39f
e7cfc20
3ef85c4
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 |
---|---|---|
|
@@ -53,6 +53,23 @@ export function setComponentProps(component, props, renderMode, context, mountAl | |
if (component.__ref) component.__ref(component); | ||
} | ||
|
||
export function catchErrorInComponent(error, component) { | ||
flushMounts(); | ||
for (; component; component = component._ancestorComponent) { | ||
if (component.componentDidCatch && !component._caught) { | ||
try { | ||
component.componentDidCatch(error); | ||
component._caught = true; | ||
enqueueRender(component); | ||
return; | ||
} catch (e) { | ||
error = e; | ||
} | ||
} | ||
} | ||
throw error; | ||
} | ||
|
||
|
||
|
||
/** | ||
|
@@ -79,129 +96,150 @@ export function renderComponent(component, renderMode, mountAll, isChild) { | |
initialChildComponent = component._component, | ||
skip = false, | ||
snapshot = previousContext, | ||
rendered, inst, cbase; | ||
|
||
if (component.constructor.getDerivedStateFromProps) { | ||
previousState = extend({}, previousState); | ||
component.state = extend(state, component.constructor.getDerivedStateFromProps(props, state)); | ||
} | ||
|
||
// if updating | ||
if (isUpdate) { | ||
component.props = previousProps; | ||
component.state = previousState; | ||
component.context = previousContext; | ||
if (renderMode!==FORCE_RENDER | ||
&& component.shouldComponentUpdate | ||
&& component.shouldComponentUpdate(props, state, context) === false) { | ||
skip = true; | ||
} | ||
else if (component.componentWillUpdate) { | ||
component.componentWillUpdate(props, state, context); | ||
} | ||
component.props = props; | ||
component.state = state; | ||
component.context = context; | ||
} | ||
|
||
component.prevProps = component.prevState = component.prevContext = component.nextBase = null; | ||
component._dirty = false; | ||
rendered, inst, cbase, | ||
exception, clearCaught = component._caught; | ||
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. I'm trying to understand the 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. The The "should bubble on repeated errors" and "should bubble on ignored errors" tests cover these cases. Repeated errors is the normal case to handle, as it isn't possible for an error boundary component to anticipate every error that could occur in fallback child tree. Components that implement 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. Thanks for taking the time to explain this - it helps! |
||
|
||
if (!skip) { | ||
rendered = component.render(props, state, context); | ||
|
||
// context to pass to the child, can be updated via (grand-)parent component | ||
if (component.getChildContext) { | ||
context = extend(extend({}, context), component.getChildContext()); | ||
try { | ||
if (component.constructor.getDerivedStateFromProps) { | ||
previousState = extend({}, previousState); | ||
component.state = extend(state, component.constructor.getDerivedStateFromProps(props, state)); | ||
} | ||
|
||
if (isUpdate && component.getSnapshotBeforeUpdate) { | ||
snapshot = component.getSnapshotBeforeUpdate(previousProps, previousState); | ||
// if updating | ||
if (isUpdate) { | ||
component.props = previousProps; | ||
component.state = previousState; | ||
component.context = previousContext; | ||
if (renderMode!==FORCE_RENDER | ||
&& component.shouldComponentUpdate | ||
&& component.shouldComponentUpdate(props, state, context) === false) { | ||
skip = true; | ||
} | ||
else if (component.componentWillUpdate) { | ||
component.componentWillUpdate(props, state, context); | ||
} | ||
component.props = props; | ||
component.state = state; | ||
component.context = context; | ||
} | ||
|
||
let childComponent = rendered && rendered.nodeName, | ||
toUnmount, base; | ||
component.prevProps = component.prevState = component.prevContext = component.nextBase = null; | ||
component._dirty = false; | ||
|
||
if (typeof childComponent==='function') { | ||
// set up high order component link | ||
if (!skip) { | ||
rendered = component.render(props, state, context); | ||
|
||
let childProps = getNodeProps(rendered); | ||
inst = initialChildComponent; | ||
// context to pass to the child, can be updated via (grand-)parent component | ||
if (component.getChildContext) { | ||
context = extend(extend({}, context), component.getChildContext()); | ||
} | ||
|
||
if (inst && inst.constructor===childComponent && childProps.key==inst.__key) { | ||
setComponentProps(inst, childProps, SYNC_RENDER, context, false); | ||
if (isUpdate && component.getSnapshotBeforeUpdate) { | ||
snapshot = component.getSnapshotBeforeUpdate(previousProps, previousState); | ||
} | ||
else { | ||
toUnmount = inst; | ||
|
||
component._component = inst = createComponent(childComponent, childProps, context); | ||
inst.nextBase = inst.nextBase || nextBase; | ||
inst._parentComponent = component; | ||
setComponentProps(inst, childProps, NO_RENDER, context, false); | ||
renderComponent(inst, SYNC_RENDER, mountAll, true); | ||
|
||
let childComponent = rendered && rendered.nodeName, | ||
toUnmount, base; | ||
try { | ||
if (typeof childComponent==='function') { | ||
// set up high order component link | ||
|
||
let childProps = getNodeProps(rendered); | ||
inst = initialChildComponent; | ||
|
||
if (inst && inst.constructor===childComponent && childProps.key==inst.__key) { | ||
setComponentProps(inst, childProps, SYNC_RENDER, context, false); | ||
} | ||
else { | ||
toUnmount = inst; | ||
|
||
component._component = inst = createComponent(childComponent, childProps, context, component); | ||
inst.nextBase = inst.nextBase || nextBase; | ||
inst._parentComponent = component; | ||
setComponentProps(inst, childProps, NO_RENDER, context, false); | ||
renderComponent(inst, SYNC_RENDER, mountAll, true); | ||
} | ||
|
||
base = inst.base; | ||
} | ||
else { | ||
cbase = initialBase; | ||
|
||
// destroy high order component link | ||
toUnmount = initialChildComponent; | ||
if (toUnmount) { | ||
cbase = component._component = null; | ||
} | ||
|
||
if (initialBase || renderMode===SYNC_RENDER) { | ||
if (cbase) cbase._component = null; | ||
base = diff(cbase, rendered, context, mountAll || !isUpdate, initialBase && initialBase.parentNode, component); | ||
} | ||
} | ||
} catch (e) { | ||
exception = e; | ||
clearCaught = false; | ||
if (!base) { | ||
base = initialBase || document.createTextNode(""); | ||
} | ||
} | ||
|
||
base = inst.base; | ||
} | ||
else { | ||
cbase = initialBase; | ||
if (initialBase && base!==initialBase && inst!==initialChildComponent) { | ||
let baseParent = initialBase.parentNode; | ||
if (baseParent && base!==baseParent) { | ||
baseParent.replaceChild(base, initialBase); | ||
|
||
// destroy high order component link | ||
toUnmount = initialChildComponent; | ||
if (toUnmount) { | ||
cbase = component._component = null; | ||
if (!toUnmount) { | ||
initialBase._component = null; | ||
recollectNodeTree(initialBase, false); | ||
} | ||
} | ||
} | ||
|
||
if (initialBase || renderMode===SYNC_RENDER) { | ||
if (cbase) cbase._component = null; | ||
base = diff(cbase, rendered, context, mountAll || !isUpdate, initialBase && initialBase.parentNode, true); | ||
if (toUnmount && toUnmount.base) { | ||
unmountComponent(toUnmount); | ||
} | ||
} | ||
|
||
if (initialBase && base!==initialBase && inst!==initialChildComponent) { | ||
let baseParent = initialBase.parentNode; | ||
if (baseParent && base!==baseParent) { | ||
baseParent.replaceChild(base, initialBase); | ||
|
||
if (!toUnmount) { | ||
initialBase._component = null; | ||
recollectNodeTree(initialBase, false); | ||
component.base = base; | ||
if (base && !isChild) { | ||
let componentRef = component, | ||
t = component; | ||
while ((t=t._parentComponent)) { | ||
(componentRef = t).base = base; | ||
} | ||
base._component = componentRef; | ||
base._componentConstructor = componentRef.constructor; | ||
} | ||
} | ||
|
||
if (toUnmount) { | ||
unmountComponent(toUnmount); | ||
if (!isUpdate || mountAll) { | ||
mounts.unshift(component); | ||
} | ||
|
||
component.base = base; | ||
if (base && !isChild) { | ||
let componentRef = component, | ||
t = component; | ||
while ((t=t._parentComponent)) { | ||
(componentRef = t).base = base; | ||
else if (!skip) { | ||
// Ensure that pending componentDidMount() hooks of child components | ||
// are called before the componentDidUpdate() hook in the parent. | ||
// Note: disabled as it causes duplicate hooks, see https://github.com/developit/preact/issues/750 | ||
// flushMounts(); | ||
|
||
if (component.componentDidUpdate) { | ||
component.componentDidUpdate(previousProps, previousState, snapshot); | ||
} | ||
base._component = componentRef; | ||
base._componentConstructor = componentRef.constructor; | ||
if (options.afterUpdate) options.afterUpdate(component); | ||
} | ||
} | ||
|
||
if (!isUpdate || mountAll) { | ||
mounts.unshift(component); | ||
} | ||
else if (!skip) { | ||
// Ensure that pending componentDidMount() hooks of child components | ||
// are called before the componentDidUpdate() hook in the parent. | ||
// Note: disabled as it causes duplicate hooks, see https://github.com/developit/preact/issues/750 | ||
// flushMounts(); | ||
|
||
if (component.componentDidUpdate) { | ||
component.componentDidUpdate(previousProps, previousState, snapshot); | ||
while (component._renderCallbacks.length) component._renderCallbacks.pop().call(component); | ||
|
||
if (clearCaught) { | ||
component._caught = false; | ||
} | ||
if (options.afterUpdate) options.afterUpdate(component); | ||
|
||
} catch (e) { | ||
catchErrorInComponent(e, component._ancestorComponent); | ||
} | ||
|
||
while (component._renderCallbacks.length) component._renderCallbacks.pop().call(component); | ||
if (typeof exception !== "undefined") { | ||
catchErrorInComponent(exception, component); | ||
} | ||
|
||
if (!diffLevel && !isChild) flushMounts(); | ||
} | ||
|
@@ -214,10 +252,12 @@ export function renderComponent(component, renderMode, mountAll, isChild) { | |
* @param {import('../vnode').VNode} vnode A Component-referencing VNode | ||
* @param {object} context The current context | ||
* @param {boolean} mountAll Whether or not to immediately mount all components | ||
* @param {import('../component').Component} [ancestorComponent] The nearest ancestor component | ||
* beneath which the new component will be mounted | ||
* @returns {import('../dom').PreactElement} The created/mutated element | ||
* @private | ||
*/ | ||
export function buildComponentFromVNode(dom, vnode, context, mountAll) { | ||
export function buildComponentFromVNode(dom, vnode, context, mountAll, ancestorComponent) { | ||
let c = dom && dom._component, | ||
originalComponent = c, | ||
oldDom = dom, | ||
|
@@ -238,7 +278,7 @@ export function buildComponentFromVNode(dom, vnode, context, mountAll) { | |
dom = oldDom = null; | ||
} | ||
|
||
c = createComponent(vnode.nodeName, props, context); | ||
c = createComponent(vnode.nodeName, props, context, ancestorComponent); | ||
if (dom && !c.nextBase) { | ||
c.nextBase = dom; | ||
// passing dom/oldDom as nextBase will recycle it if unused, so bypass recycling on L229: | ||
|
@@ -264,13 +304,20 @@ export function buildComponentFromVNode(dom, vnode, context, mountAll) { | |
* @private | ||
*/ | ||
export function unmountComponent(component) { | ||
if (component._disable) return; | ||
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. Should this check be the first thing that happens in this function (in other words, before the call to 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. Definitely an oversight that the check against |
||
component._disable = true; | ||
|
||
if (options.beforeUnmount) options.beforeUnmount(component); | ||
|
||
let base = component.base; | ||
|
||
component._disable = true; | ||
|
||
if (component.componentWillUnmount) component.componentWillUnmount(); | ||
if (component.componentWillUnmount) { | ||
try { | ||
component.componentWillUnmount(); | ||
} catch (e) { | ||
catchErrorInComponent(e, component._ancestorComponent); | ||
} | ||
} | ||
|
||
component.base = null; | ||
|
||
|
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.
Do we need another try/catch here? I would think that if
componentDidCatch
exists, the error is passed to it & it ends there. If the method does not exist, it's uncaught so the component (tree) is unmounted.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.
Errors that occur in
componentDidCatch
should bubble to ancestors, regardless of what lifecycle method generated the error or from what path that lifecycle method occurred. Error paths should be rare, so it's fine for it to be slow.