Skip to content
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

Closed
wants to merge 33 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d54d337
Add support for Component.componentDidCatch variant with single argument
rpetrich Sep 20, 2017
6ff8cc6
Make root components have their _ancestorComponent property set to un…
rpetrich Sep 20, 2017
721b437
Merge branch 'master' into componentDidCatch-support
developit Jan 25, 2018
dde11b2
Merge branch 'master' into componentDidCatch-support
developit Feb 23, 2018
e105500
Merge remote-tracking branch 'upstream/master' into componentDidCatch…
rpetrich May 8, 2018
3705d4e
Merge remote-tracking branch 'upstream/master' into componentDidCatch…
rpetrich May 14, 2018
e8776c7
Catch errors that occur during async renders
rpetrich May 15, 2018
980b4cb
Catch and propagate errors that occur in componentWillUnmount
rpetrich May 15, 2018
89efb5e
Mount a component if it's about to receive an error and re-render any…
rpetrich May 15, 2018
b671425
Propagate a errors up the chain when a component renders children tha…
rpetrich May 15, 2018
31f5fd9
Merge remote-tracking branch 'origin/componentDidCatch-support' into …
rpetrich May 15, 2018
3a3d7cb
Merge branch 'master' into componentDidCatch-support
reznord May 16, 2018
313ffe6
Merge branch 'master' into componentDidCatch-support
reznord May 19, 2018
d4612f4
Use fewer try/catch boundaries to support componentDidCatch
rpetrich May 22, 2018
1f164dd
Merge remote-tracking branch 'upstream/master' into componentDidCatch…
rpetrich May 29, 2018
b35c534
Test new lifecycle methods for componentDidCatch support
rpetrich May 29, 2018
aa48769
Use a new _caught property to determine if a component has just caugh…
rpetrich May 30, 2018
a2b4c8c
Flush mount events before dispatching errors
rpetrich May 30, 2018
4320ff9
Send componentDidMount to a newly created parent only once when a chi…
rpetrich May 30, 2018
4338a7c
Ensure that componentWillUnmount is sent only once, even in error paths
rpetrich May 30, 2018
74fddde
Remove error parameter in componentDidCatch (which triggered linter w…
rpetrich May 30, 2018
65ff737
Merge remote-tracking branch 'upstream/master' into componentDidCatch…
rpetrich May 30, 2018
8b23777
Enqueue renders triggered from componentDidCatch instead of invoking …
rpetrich May 30, 2018
308d8aa
Fully debounce rendering during lifecycle tests (because error handli…
rpetrich May 30, 2018
b853143
Merge remote-tracking branch 'origin/componentDidCatch-support' into …
rpetrich May 30, 2018
30cde7b
Avoid infinite rerender loop when top-most error boundary attempts to…
rpetrich May 30, 2018
b98159a
Merge remote-tracking branch 'upstream/master' into componentDidCatch…
rpetrich Jun 5, 2018
286e6ea
Add test to verify that componentDidCatch errors stop bubbling once c…
rpetrich Jun 5, 2018
be4dee5
Add JSDoc comments for componentRoot/ancestorComponent parameters
rpetrich Jun 5, 2018
a019562
Check if component is disabled before dispatching beforeUnmount hook …
rpetrich Jun 5, 2018
3a2a39f
Merge branch 'master' into componentDidCatch-support
rpetrich Jun 12, 2018
e7cfc20
Merge branch 'master' into componentDidCatch-support
reznord Jul 12, 2018
3ef85c4
Merge remote-tracking branch 'upstream/master' into componentDidCatch…
rpetrich Aug 5, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/properties.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"$prevProps": "__p",
"$prevState": "__s",
"$_parentComponent": "__u",
"$_ancestorComponent": "__a",
"$_componentConstructor": "_componentConstructor",
"$__html": "__html",
"$_component": "_component",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"transpile:esm": "rollup -c config/rollup.config.esm.js",
"transpile:debug": "babel debug/ -o debug.js -s",
"transpile": "npm-run-all transpile:main transpile:esm transpile:devtools transpile:debug",
"optimize": "uglifyjs dist/preact.dev.js -c conditionals=false,sequences=false,loops=false,join_vars=false,collapse_vars=false --pure-funcs=Object.defineProperty --mangle-props --mangle-regex=\"/^(_|normalizedNodeName|nextBase|prev[CPS]|_parentC)/\" --name-cache config/properties.json -b width=120,quote_style=3 -o dist/preact.js -p relative --in-source-map dist/preact.dev.js.map --source-map dist/preact.js.map",
"optimize": "uglifyjs dist/preact.dev.js -c conditionals=false,sequences=false,loops=false,join_vars=false,collapse_vars=false --pure-funcs=Object.defineProperty --mangle-props --mangle-regex=\"/^(_|normalizedNodeName|nextBase|prev[CPS]|_parentC|_ancestorC)/\" --name-cache config/properties.json -b width=120,quote_style=3 -o dist/preact.js -p relative --in-source-map dist/preact.dev.js.map --source-map dist/preact.js.map",
"minify": "uglifyjs dist/preact.js -c collapse_vars,evaluate,screw_ie8,unsafe,loops=false,keep_fargs=false,pure_getters,unused,dead_code -m -o dist/preact.min.js -p relative --in-source-map dist/preact.js.map --source-map dist/preact.min.js.map",
"strip:main": "jscodeshift --run-in-band -s -t config/codemod-strip-tdz.js dist/preact.dev.js && jscodeshift --run-in-band -s -t config/codemod-const.js dist/preact.dev.js && jscodeshift --run-in-band -s -t config/codemod-let-name.js dist/preact.dev.js",
"strip:esm": "jscodeshift --run-in-band -s -t config/codemod-strip-tdz.js dist/preact.esm.js && jscodeshift --run-in-band -s -t config/codemod-const.js dist/preact.esm.js && jscodeshift --run-in-band -s -t config/codemod-let-name.js dist/preact.esm.js",
Expand Down
1 change: 1 addition & 0 deletions src/preact.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ declare namespace preact {
shouldComponentUpdate?(nextProps: Readonly<P>, nextState: Readonly<S>, nextContext: any): boolean;
componentWillUpdate?(nextProps: Readonly<P>, nextState: Readonly<S>, nextContext: any): void;
componentDidUpdate?(previousProps: Readonly<P>, previousState: Readonly<S>, previousContext: any): void;
componentDidCatch?(error: any): void;
}

abstract class Component<P, S> {
Expand Down
2 changes: 1 addition & 1 deletion src/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ import { diff } from './vdom/diff';
* render(<Thing name="one" />, document.querySelector('#foo'));
*/
export function render(vnode, parent, merge) {
return diff(merge, vnode, {}, false, parent, false);
return diff(merge, vnode, {}, false, parent);
}
4 changes: 2 additions & 2 deletions src/vdom/component-recycler.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function collectComponent(component) {


/** Create a component. Normalizes differences between PFC's and classful Components. */
export function createComponent(Ctor, props, context) {
export function createComponent(Ctor, props, context, ancestorComponent) {
let list = components[Ctor.name],
inst;

Expand All @@ -28,7 +28,7 @@ export function createComponent(Ctor, props, context) {
inst.constructor = Ctor;
inst.render = doRender;
}

inst._ancestorComponent = ancestorComponent;

if (list) {
for (let i=list.length; i--; ) {
Expand Down
81 changes: 51 additions & 30 deletions src/vdom/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ export function setComponentProps(component, props, opts, context, mountAll) {
if (component.__ref) component.__ref(component);
}

export function catchErrorInComponent(error, component) {
for (; component; component = component._ancestorComponent) {
if (component.componentDidCatch) {
try {
Copy link
Member

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.

Copy link
Contributor Author

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.

return component.componentDidCatch(error);
} catch (e) {
error = e;
}
}
}
throw error;
}


/** Render a Component, triggering necessary lifecycle events and taking High-Order Components into account.
Expand Down Expand Up @@ -105,40 +117,45 @@ export function renderComponent(component, opts, mountAll, isChild) {
let childComponent = rendered && rendered.nodeName,
toUnmount, base;

if (typeof childComponent==='function') {
// set up high order component link
try {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the try/catch I was referring to in my other review comment.

We can/should call catchErrorInComponent from within here directly to avoid the extra layer(s) of try/catch blocks.

if (typeof childComponent==='function') {
// set up high order component link

let childProps = getNodeProps(rendered);
inst = initialChildComponent;
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;
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);
}

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);
base = inst.base;
}
else {
cbase = initialBase;

base = inst.base;
}
else {
cbase = initialBase;

// destroy high order component link
toUnmount = initialChildComponent;
if (toUnmount) {
cbase = component._component = null;
}
// destroy high order component link
toUnmount = initialChildComponent;
if (toUnmount) {
cbase = component._component = null;
}

if (initialBase || opts===SYNC_RENDER) {
if (cbase) cbase._component = null;
base = diff(cbase, rendered, context, mountAll || !isUpdate, initialBase && initialBase.parentNode, true);
if (initialBase || opts===SYNC_RENDER) {
if (cbase) cbase._component = null;
base = diff(cbase, rendered, context, mountAll || !isUpdate, initialBase && initialBase.parentNode, component);
}
}
} catch (e) {
base = initialBase || document.createTextNode("");
catchErrorInComponent(e, component);
}

if (initialBase && base!==initialBase && inst!==initialChildComponent) {
Expand Down Expand Up @@ -179,7 +196,11 @@ export function renderComponent(component, opts, mountAll, isChild) {
// flushMounts();

if (component.componentDidUpdate) {
component.componentDidUpdate(previousProps, previousState, previousContext);
try {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try/catch is expensive, can we avoid doing this unless the component or an ancestor implements componentDidCatch?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the check would be more expensive because you have to check every ancestor of the component, which means the complexity goes up as your tree gets deeper. Its really a question of O(n) vs O(1), and with modern browsers, try/catch isn't even that expensive.

https://jsperf.com/try-catch-jsperf

Created a JSPerf to check, and it seems like the checking for the existence of the method on just the component is more expensive then a standard try-catch

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 thanks for checking

component.componentDidUpdate(previousProps, previousState, previousContext);
} catch (e) {
catchErrorInComponent(e, component._ancestorComponent);
}
}
if (options.afterUpdate) options.afterUpdate(component);
}
Expand All @@ -199,7 +220,7 @@ export function renderComponent(component, opts, mountAll, isChild) {
* @returns {Element} dom 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,
Expand All @@ -220,7 +241,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:
Expand Down
43 changes: 25 additions & 18 deletions src/vdom/diff.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ATTR_KEY } from '../constants';
import { isSameNodeType, isNamedNode } from './index';
import { buildComponentFromVNode } from './component';
import { buildComponentFromVNode, catchErrorInComponent } from './component';
import { createNode, setAccessor } from '../dom/index';
import { unmountComponent } from './component';
import options from '../options';
Expand All @@ -23,7 +23,13 @@ export function flushMounts() {
let c;
while ((c=mounts.pop())) {
if (options.afterMount) options.afterMount(c);
if (c.componentDidMount) c.componentDidMount();
if (c.componentDidMount) {
try {
c.componentDidMount();
} catch (e) {
catchErrorInComponent(e, c._ancestorComponent);
}
}
}
}

Expand All @@ -44,19 +50,20 @@ export function diff(dom, vnode, context, mountAll, parent, componentRoot) {
hydrating = dom!=null && !(ATTR_KEY in dom);
}

let ret = idiff(dom, vnode, context, mountAll, componentRoot);

// append the element if its a new parent
if (parent && ret.parentNode!==parent) parent.appendChild(ret);

// diffLevel being reduced to 0 means we're exiting the diff
if (!--diffLevel) {
hydrating = false;
// invoke queued componentDidMount lifecycle methods
if (!componentRoot) flushMounts();
let ret;
try {
return ret = idiff(dom, vnode, context, mountAll, componentRoot);
} finally {
// append the element if its a new parent
if (ret && parent && ret.parentNode!==parent) parent.appendChild(ret);

// diffLevel being reduced to 0 means we're exiting the diff
if (!--diffLevel) {
hydrating = false;
// invoke queued componentDidMount lifecycle methods
if (!componentRoot) flushMounts();
}
}

return ret;
}


Expand Down Expand Up @@ -97,7 +104,7 @@ function idiff(dom, vnode, context, mountAll, componentRoot) {
// If the VNode represents a Component, perform a component diff:
let vnodeName = vnode.nodeName;
if (typeof vnodeName==='function') {
return buildComponentFromVNode(dom, vnode, context, mountAll);
return buildComponentFromVNode(dom, vnode, context, mountAll, componentRoot);
}


Expand Down Expand Up @@ -140,7 +147,7 @@ function idiff(dom, vnode, context, mountAll, componentRoot) {
}
// otherwise, if there are existing or new children, diff them:
else if (vchildren && vchildren.length || fc!=null) {
innerDiffNode(out, vchildren, context, mountAll, hydrating || props.dangerouslySetInnerHTML!=null);
innerDiffNode(out, vchildren, context, mountAll, hydrating || props.dangerouslySetInnerHTML!=null, componentRoot);
}


Expand All @@ -162,7 +169,7 @@ function idiff(dom, vnode, context, mountAll, componentRoot) {
* @param {Boolean} mountAll
* @param {Boolean} isHydrating If `true`, consumes externally created elements similar to hydration
*/
function innerDiffNode(dom, vchildren, context, mountAll, isHydrating) {
function innerDiffNode(dom, vchildren, context, mountAll, isHydrating, componentRoot) {
let originalChildren = dom.childNodes,
children = [],
keyed = {},
Expand Down Expand Up @@ -217,7 +224,7 @@ function innerDiffNode(dom, vchildren, context, mountAll, isHydrating) {
}

// morph the matched/found/created DOM child to match vchild (deep)
child = idiff(child, vchild, context, mountAll);
child = idiff(child, vchild, context, mountAll, componentRoot);

f = originalChildren[i];
if (child && child!==dom && child!==f) {
Expand Down
46 changes: 46 additions & 0 deletions test/browser/components.js
Original file line number Diff line number Diff line change
Expand Up @@ -806,4 +806,50 @@ describe('Components', () => {
expect(C3.prototype.componentWillMount, 'inject between, C3 w/ intermediary div').to.have.been.calledOnce;
});
});

describe('Properties', () => {
let ancestorComponent;
let childComponent;
class Ancestor extends Component {
constructor(props, context) {
super(props, context);
ancestorComponent = this;
}
render() {
return this.props.children[0];
}
}
class Child extends Component {
constructor(props, context) {
super(props, context);
childComponent = this;
}
render() {
return <div/>;
}
}
const HighOrderChild = () => <Child/>;
it('should have correct ancestor/parent with immediate child', () => {
render(<Ancestor><Child/></Ancestor>, scratch);
expect(childComponent._ancestorComponent).to.equal(ancestorComponent);
expect(childComponent._parentComponent).to.equal(ancestorComponent);
expect(ancestorComponent._ancestorComponent).to.equal(undefined);
expect(ancestorComponent._parentComponent).to.equal(undefined);
});
it('should have correct ancestor/parent with grandchild', () => {
render(<Ancestor><div><Child/></div></Ancestor>, scratch);
expect(childComponent._ancestorComponent).to.equal(ancestorComponent);
expect(childComponent._parentComponent).to.equal(undefined);
expect(ancestorComponent._ancestorComponent).to.equal(undefined);
expect(ancestorComponent._parentComponent).to.equal(undefined);
});
it('should have correct ancestor/parent with high-order grandchild', () => {
render(<Ancestor><div><HighOrderChild/></div></Ancestor>, scratch);
expect(childComponent._ancestorComponent).to.not.equal(undefined);
expect(childComponent._ancestorComponent).to.equal(childComponent._parentComponent);
expect(childComponent._ancestorComponent._ancestorComponent).to.equal(ancestorComponent);
expect(ancestorComponent._ancestorComponent).to.equal(undefined);
expect(ancestorComponent._parentComponent).to.equal(undefined);
});
});
});
Loading