diff --git a/doc/reference/store.md b/doc/reference/store.md index 3be25d63..b35d6af1 100644 --- a/doc/reference/store.md +++ b/doc/reference/store.md @@ -252,19 +252,27 @@ The `useStore` hook is used to select some part of the store state. It accepts two arguments: - a selector function, which takes the store state as first argument (and the - component props as second argument) and returns - an object or an array (which will be then observed), -- optionally, an object with a `store` key (if we want to override the default - store) and an equality function (if we want to specialize the comparison). + component props as second argument) and which must return the part of the + store state that will be made available and observed for changes, +- optionally, an object which can have the following optional keys: + - a `store` key containing a store object if we want to use another store than + the default store, + - an `isEqual` key containing an equality function if we want to specialize + the comparison (the function must accept two arguments: the previous result + and the new result, and must return whether they are equal), + - and an `onUpdate` key containing an update function if we want to execute an + arbitrary code every time the selected state changes (the function will + receive one argument, the new result, and can execute arbitrary code). -If the `useStore` callback selects a sub part of the store state, the component +If the `useStore` selector returns a sub part of the store state, the component will only be rerendered whenever this part of the state changes. Otherwise, it -will perform a strict equality check and will update the component every time this -check fails. +will perform a strict equality check (unless the `isEqual` option is defined, +then it will call it) and will update the component every time this check fails. -Also, it may not be obvious, but it is crucial to remember that the selector -function should return an object or an array. The reason is that it needs to be -observed, otherwise the component would not be able to react to changes. +Note that if the selector function returns a primitive type, the result of +`useStore` will be immutable and it will not react to changes. In this case, it +is important to define the `onUpdate` option to properly update the value +manually when it changes. ### `useDispatch` diff --git a/src/store.ts b/src/store.ts index 12478aa5..3a07c77d 100644 --- a/src/store.ts +++ b/src/store.ts @@ -77,6 +77,7 @@ export class Store extends Context { interface SelectorOptions { store?: Store; isEqual?: (a: any, b: any) => boolean; + onUpdate?: (result: any) => any; } const isStrictEqual = (a, b) => a === b; @@ -89,7 +90,7 @@ export function useStore(selector, options: SelectorOptions = {}): any { } let result = selector(store.state, component.props); const hashFn = store.observer.revNumber.bind(store.observer); - let revNumber = hashFn(result) || result; + let revNumber = hashFn(result); const isEqual = options.isEqual || isStrictEqual; if (!store.updateFunctions[component.__owl__.id]) { store.updateFunctions[component.__owl__.id] = []; @@ -104,6 +105,9 @@ export function useStore(selector, options: SelectorOptions = {}): any { (newRevNumber === 0 && !isEqual(oldResult, result)) ) { revNumber = newRevNumber; + if (options.onUpdate) { + options.onUpdate(result); + } return true; } return false; @@ -122,6 +126,9 @@ export function useStore(selector, options: SelectorOptions = {}): any { delete store.updateFunctions[component.__owl__.id]; result = selector(store.state, props); }); + if (typeof result !== "object") { + return result; + } return new Proxy(result, { get(target, k) { return result[k]; diff --git a/tests/store_hooks.test.ts b/tests/store_hooks.test.ts index 75dea74a..968cef53 100644 --- a/tests/store_hooks.test.ts +++ b/tests/store_hooks.test.ts @@ -48,6 +48,42 @@ describe("connecting a component to store", () => { expect(fixture.innerHTML).toBe("
hello
"); }); + test("useStore can observe primitive types and call onUpdate", async () => { + const state = { isBoolean: false }; + const actions = { + setTrue({ state }) { + state.isBoolean = true; + } + }; + const store = new Store({ state, actions }); + + class App extends Component { + static template = xml` +
+ ok +
`; + isBoolean: boolean; + constructor() { + super(); + this.isBoolean = useStore(state => state.isBoolean, { + onUpdate: isBoolean => { + this.isBoolean = isBoolean; + } + }); + } + } + + (env).store = store; + const app = new App(); + + await app.mount(fixture); + expect(fixture.innerHTML).toBe("
"); + + store.dispatch("setTrue"); + await nextTick(); + expect(fixture.innerHTML).toBe("
ok
"); + }); + test("throw error if no store is found", async () => { class App extends Component { static template = xml`
`;