mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[FIX] store: properly render, even if shouldupdate is implemented
Before this commit, an unwanted behaviour happened when using components with shouldUpdate implemented, and store/state. The actual problem is the following: the sub component is using a store, and shouldupdate. Whenever the component is rendered, it checks if there is an incoming rendering from the context. If that is the case, it skips the rendering, because we actually only want to be rendered by the store rendering (otherwise, we may run into issue with inconsistent data (more recent data from the context, older data in the component). However, if shouldUpdate is implemented, then the rendering coming from the context simply does not arrive. To fix this, we tried to just force these renderings to go through all children, regardless of their shouldUpdate status. This actually works, but then shouldUpdate is ignored, which is an issue in Odoo discuss. Then, after discussing this situation, we noticed that the context/store system is actually unsafe: the protection given by the check mentioned above is in fact fundamentally insufficient: there are other perfectly valid situations where a rendering can be triggered on the component, which will bypass the check (for example, an explicit call to this.render() or a rendering initiated by some parent component) and cause a crash if the component is not properly defensively written. Therefore, it is currently mandatory for all components using context/store to be aware of that, and to protect themselves against such situations. So, in that regard, the check is not really a protection, it just helps hiding an unsafe situation anyway and we decided to remove it. Note that this is a potentially breaking change: components using a store and some local state will now be rendered twice in some cases... closes #799
This commit is contained in:
@@ -115,16 +115,6 @@ export function useContextWithCB(ctx: Context, component: Component, method): an
|
||||
__owl__.observer = new Observer();
|
||||
__owl__.observer.notifyCB = component.render.bind(component);
|
||||
}
|
||||
const currentCB = __owl__.observer.notifyCB;
|
||||
__owl__.observer.notifyCB = function () {
|
||||
if (ctx.rev > mapping[id]) {
|
||||
// in this case, the context has been updated since we were rendering
|
||||
// last, and we do not need to render here with the observer. A
|
||||
// rendering is coming anyway, with the correct props.
|
||||
return;
|
||||
}
|
||||
currentCB();
|
||||
};
|
||||
|
||||
mapping[id] = 0;
|
||||
const renderFn = __owl__.renderFn;
|
||||
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
import { Component } from "./component/component";
|
||||
import { Env } from "./component/component";
|
||||
import { Component, Env } from "./component/component";
|
||||
import { Context, useContextWithCB } from "./context";
|
||||
import { onWillUpdateProps } from "./hooks";
|
||||
|
||||
|
||||
+20
-1
@@ -289,7 +289,26 @@ describe("Context", () => {
|
||||
expect(testContext.subscriptions.update.length).toBe(0);
|
||||
});
|
||||
|
||||
test("concurrent renderings", async () => {
|
||||
test.skip("concurrent renderings", async () => {
|
||||
/**
|
||||
* Note: this test is interesting, but sadly just an incomplete attempt at
|
||||
* protecting users against themselves. With the context API, it is not
|
||||
* possible for the framework to protect completely against crashes. Maybe
|
||||
* like in this case, when a component is in a simple hierarchy where all
|
||||
* renderings come from the context changes, but in a real case, where some
|
||||
* code can trigger a rendering independently, it is insufficient.
|
||||
*
|
||||
* The main problem is that the sub component depends on some external state,
|
||||
* which may be modified, and then incompatible with the component actual
|
||||
* state (for example, if the sub component has an id key related to some
|
||||
* object that has been removed from the context).
|
||||
*
|
||||
* For now, sadly, the only solution is that components that depends on external
|
||||
* state should guarantee their own integrity themselves. Then maybe this
|
||||
* could be solved at the level of a state management solution that has a
|
||||
* more advanced API, to let components determine if they should be updated
|
||||
* or not (so, something slightly more advanced that the useStore hook).
|
||||
*/
|
||||
const testContext = new Context({ x: { n: 1 }, key: "x" });
|
||||
const def = makeDeferred();
|
||||
let stateC;
|
||||
|
||||
+105
-1
@@ -1,4 +1,4 @@
|
||||
import { Component, Env } from "../src/component/component";
|
||||
import { Component, Env, mount } from "../src/component/component";
|
||||
import { Store, useStore, useDispatch, useGetters, EnvWithStore } from "../src/store";
|
||||
import { useState } from "../src/hooks";
|
||||
import { xml } from "../src/tags";
|
||||
@@ -1241,4 +1241,108 @@ describe("various scenarios", () => {
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("component with store, useState and shouldUpdate=false", async () => {
|
||||
let state: any;
|
||||
|
||||
const store = new Store({ state: { rev: 0 } });
|
||||
|
||||
class Child extends Component {
|
||||
static template = xml`<div><t t-esc="state.word"/><t t-esc="props.name"/></div>`;
|
||||
|
||||
state = useState({ word: "hello" });
|
||||
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
state = this.state;
|
||||
useStore((props) => {
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
|
||||
shouldUpdate() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`<div><Child name="state.name"/></div>`;
|
||||
static components = { Child };
|
||||
|
||||
state = useState({ name: "World" });
|
||||
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
useStore((props) => store.state.rev);
|
||||
}
|
||||
}
|
||||
(env as any).store = store;
|
||||
|
||||
await mount(Parent, { target: fixture, env });
|
||||
expect(fixture.innerHTML).toBe("<div><div>helloWorld</div></div>");
|
||||
|
||||
store.state.rev++;
|
||||
// this is the key to the bug, it makes Parent be in "render" state but not
|
||||
// yet rendered while the change of state happens
|
||||
await Promise.resolve();
|
||||
state.word = "test";
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><div>testWorld</div></div>");
|
||||
});
|
||||
|
||||
test("component with store, useState, shouldUpdate=false and child with shouldupdate false", async () => {
|
||||
let state: any;
|
||||
|
||||
const store = new Store({ state: { rev: 0 } });
|
||||
|
||||
class ChildChild extends Component {
|
||||
static template = xml`<div><t t-esc="props.value"/></div>`;
|
||||
|
||||
shouldUpdate() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class Child extends Component {
|
||||
static template = xml`<div><t t-esc="state.word"/><t t-esc="props.name"/><ChildChild value="state.value"/></div>`;
|
||||
static components = { ChildChild };
|
||||
state = useState({ word: "hello", value: 3 });
|
||||
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
state = this.state;
|
||||
useStore((props) => {
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
|
||||
shouldUpdate() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`<div><Child name="state.name"/></div>`;
|
||||
static components = { Child };
|
||||
|
||||
state = useState({ name: "World" });
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
useStore((props) => store.state.rev);
|
||||
}
|
||||
}
|
||||
(env as any).store = store;
|
||||
|
||||
await mount(Parent, { target: fixture, env });
|
||||
expect(fixture.innerHTML).toBe("<div><div>helloWorld<div>3</div></div></div>");
|
||||
|
||||
store.state.rev++;
|
||||
// this is the key to the bug, it makes Parent be in "render" state but not
|
||||
// yet rendered while the change of state happens
|
||||
await Promise.resolve();
|
||||
state.word = "test";
|
||||
state.value = 44;
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("<div><div>testWorld<div>3</div></div></div>");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user