diff --git a/src/component/component_node.ts b/src/component/component_node.ts index 749df4f4..cbeef028 100644 --- a/src/component/component_node.ts +++ b/src/component/component_node.ts @@ -137,21 +137,32 @@ export class ComponentNode } async render() { - let fiber = this.fiber; - if (fiber && !fiber.bdom && !fibersInError.has(fiber)) { - return fiber.root.promise; + const current = this.fiber; + if (current && !current.bdom && !fibersInError.has(current)) { + return current.root.promise; } - if (!this.bdom && !fiber) { + if (!this.bdom && !current) { // should find a way to return the future mounting promise return; } - fiber = makeRootFiber(this); + const fiber = makeRootFiber(this); this.app.scheduler.addFiber(fiber); await Promise.resolve(); if (this.status === STATUS.DESTROYED) { return; } - if (this.fiber === fiber) { + // We only want to actually render the component if the following two + // conditions are true: + // * this.fiber: it could be null, in which case the render has been cancelled + // * (current || !fiber.parent): if current is not null, this means that the + // render function was called when a render was already occurring. In this + // case, the pending rendering was cancelled, and the fiber needs to be + // rendered to complete the work. If current is null, we check that the + // fiber has no parent. If that is the case, the fiber was downgraded from + // a root fiber to a child fiber in the previous microtick, because it was + // embedded in a rendering coming from above, so the fiber will be rendered + // in the next microtick anyway, so we should not render it again. + if (this.fiber && (current || !fiber.parent)) { this._render(fiber); } return fiber.root.promise; diff --git a/tests/components/__snapshots__/concurrency.test.ts.snap b/tests/components/__snapshots__/concurrency.test.ts.snap index e6907f31..d5204d45 100644 --- a/tests/components/__snapshots__/concurrency.test.ts.snap +++ b/tests/components/__snapshots__/concurrency.test.ts.snap @@ -1044,6 +1044,30 @@ exports[`destroying/recreating a subwidget with different props (if start is not }" `; +exports[`parent and child rendered at exact same time 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; + + return function template(ctx, node, key = \\"\\") { + return text(ctx['props'].value); + } +}" +`; + +exports[`parent and child rendered at exact same time 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers; + + return function template(ctx, node, key = \\"\\") { + return component(\`Child\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx); + } +}" +`; + exports[`properly behave when destroyed/unmounted while rendering 1`] = ` "function anonymous(bdom, helpers ) { diff --git a/tests/components/concurrency.test.ts b/tests/components/concurrency.test.ts index 3f8a2743..4512ebae 100644 --- a/tests/components/concurrency.test.ts +++ b/tests/components/concurrency.test.ts @@ -2562,6 +2562,59 @@ test("two renderings initiated between willPatch and patched", async () => { Object.freeze(steps); }); +test("parent and child rendered at exact same time", async () => { + let child: any = null; + let steps: any[] = []; + + class Child extends Component { + static template = xml``; + setup() { + child = this; + useLogLifecycle(steps); + } + } + + class Parent extends Component { + static template = xml``; + static components = { Child }; + state = { value: 0 }; + setup() { + useLogLifecycle(steps); + } + } + + const parent = await mount(Parent, fixture); + + expect(fixture.innerHTML).toBe("0"); + + parent.state.value = 1; + parent.render(); + child.render(); + await nextTick(); + expect(fixture.innerHTML).toBe("1"); + expect(steps).toEqual([ + "Parent:setup", + "Parent:willStart", + "Parent:willRender", + "Child:setup", + "Child:willStart", + "Parent:rendered", + "Child:willRender", + "Child:rendered", + "Child:mounted", + "Parent:mounted", + "Parent:willRender", + "Child:willUpdateProps", + "Parent:rendered", + "Child:willRender", + "Child:rendered", + "Parent:willPatch", + "Child:willPatch", + "Child:patched", + "Parent:patched", + ]); +}); + // test.skip("components with shouldUpdate=false", async () => { // const state = { p: 1, cc: 10 };