diff --git a/doc/hooks.md b/doc/hooks.md index 87801de0..24e5c08c 100644 --- a/doc/hooks.md +++ b/doc/hooks.md @@ -12,6 +12,8 @@ - [`onWillUnmount`](#onwillunmount) - [`onWillPatch`](#onwillpatch) - [`onPatched`](#onpatched) + - [`onWillStart`](#onwillstart) + - [`onWillUpdateProps`](#onwillupdateprops) - [`useContext`](#usecontext) - [`useRef`](#useref) - [`useSubEnv`](#usesubenv) @@ -203,6 +205,59 @@ before the component patched. abstractions. `onPatched` registers a callback, which will be called just after the component patched. +### `onWillStart` + +`onWillStart` is an asynchronous hook. This means that the function registered +in the hook will be run just before the component is first rendered and can return a +promise, to express the fact that it is an asynchronous operation. + +Note that if there are more than one `onWillStart` registered callback, then they +will all be run in parallel. + +It can be used to load some initial data. For example, the following hook will +automatically load some data from the server, and return an object that will +be ready whenever the component is rendered: + +```js +function useLoader() { + const component = Component.current; + const record = useState({}); + onWillStart(async () => { + const recordId = component.props.id; + Object.assign(record, await fetchSomeRecord(recordId)); + }); + return record; +} +``` + +Note that this example does not update the record value whenever props are +updated. For that situation, we need to use the `onWillUpdateProps` hook. + +### `onWillUpdateProps` + +Just like `onWillStart`, `onWillUpdateProps` is an asynchronous hook. It is +designed to be run whenever the component props are updated. This could be +useful to perform some asynchronous task such as fetching updated data. + +```js +function useLoader() { + const component = Component.current; + const record = useState({}); + + async function updateRecord(id) { + Object.assign(record, await fetchSomeRecord(id)); + } + + onWillStart(() => updateRecord(component.props.id)); + onWillUpdateProps(nextProps => updateRecord(nextProps.id)); + + return record; +} +``` + +Note that if there are more than one `onWillUpdateProps` registered callback, +then they will all be run in parallel. + ### `useContext` See [`useContext`](context.md#usecontext) for reference documentation. diff --git a/doc/readme.md b/doc/readme.md index e79c461d..ea10525b 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -16,10 +16,12 @@ owl EventBus Observer hooks + onWillStart onMounted - onWillUnmount + onWillUpdateProps onWillPatch onPatched + onWillUnmount useContext useState useRef diff --git a/doc/store.md b/doc/store.md index 7f50709b..7dcc96da 100644 --- a/doc/store.md +++ b/doc/store.md @@ -9,6 +9,7 @@ - [Actions](#actions) - [Getters](#getters) - [Connecting a Component](#connecting-a-component) + - [`useStore`](#usestore) - [Semantics](#semantics) - [Good Practices](#good-practices) @@ -111,6 +112,14 @@ const actions = { }; ``` +The first argument to an action method is an object with four keys: +- `state`: the current state of the store content, +- `dispatch`: a function that can be used to dispatch other actions, +- `getters`: an object containing all getters defined in the store +- `env`: the current environment. This is useful sometimes, in particular if + an action needs to apply some side effects (such as performing an rpc), and + the `rpc` method is located in the environment. + Actions are called with the `dispatch` method on the store, and can receive an arbitrary number of arguments. @@ -196,76 +205,66 @@ is a string or a number. ### Connecting a Component -At some point, we need a way to access the state in the store from a component. -By default, an Owl `Component` is not connected to any store. To do that, we -need to create a component inheriting from `OwlComponent`: +At some point, we need a way to interact with the store from a component. This +can be done with the help of the three store hooks: + +- `useStore` to subscribe a component to some part of the store state, +- `useDispatch` to get a reference to a dispatch function +- `useGetters` to get a reference to the getters defined in the store. + + +Assume we have this store: ```javascript const actions = { increment({ state }, val) { - state.counter += val; + state.counter.value += val; } }; const state = { - counter: 0 + counter: {value: 0} }; const store = new owl.Store({ state, actions }); +``` -class Counter extends owl.ConnectedComponent { - static mapStoreToProps(state) { - return { - value: state.counter - }; - } - increment() { - this.env.store.dispatch("increment"); - } +A counter component can then select this value and dispatch an action like this: + +```js +class Counter extends Component { + counter = useStore(state => state.counter); + dispatch = useDispatch(); } const counter = new Counter({ store, qweb }); ``` -```xml - -``` - -The `ConnectedComponent` class can be configured with the following fields: - -- `mapStoreToProps`: a function that extracts the `props` of the Component - from the `state` of the `Store` and returns them as a dict. -- `getStore`: a function that takes the `env` in arguments and returns an - instance of `Store` to connect to (if not given, connects to `env.store`) -- `hashFunction`: the function to use to detect changes in the state (if not - given, generates a function that uses revision numbers, incremented at - each state change) -- `deep` (boolean): [only useful if no hashFunction is given] if `false`, only watch - for top level state changes (`true` by default) - -Note that the class `ConnectedComponent` has a `dispatch` method. This means -that the previous example could be simplified like this: - -```javascript -class Counter extends owl.ConnectedComponent { - static mapStoreToProps(state) { - return { - value: state.counter - }; - } -} -``` - ```xml ``` +### `useStore`` + +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) + + +If the `useStore` callback selects 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 updates the component every time this +check fails. + ### Semantics -The `Store` and the `ConnectedComponent` try to be smart and to optimize as much +The `Store` class and the `useStore` hook try to be smart and to optimize as much as possible the rendering and update process. What is important to know is: - components are always updated in the order of their creation (so, parent @@ -283,8 +282,9 @@ as possible the rendering and update process. What is important to know is: - avoid asynchronous components as much as possible. Asynchronous components lead to situations where parts of the UI is not updated immediately. - do not be afraid to connect many components, parent or children if needed. For - example, a `MessageList` component could get a list of ids in its `mapStoreToProps` and a `Message` component could get the data of its own + example, a `MessageList` component could get a list of ids in its `useStore` + call and a `Message` component could get the data of its own message -- since the `mapStoreToProps` function is called for each connected component, +- since the `useStore` function is called for each connected component, for each state update, it is important to make sure that these functions are as fast as possible. diff --git a/src/context.ts b/src/context.ts index 59333a3e..a7f6c4ff 100644 --- a/src/context.ts +++ b/src/context.ts @@ -40,6 +40,11 @@ export class Context extends EventBus { * next. However, the only things that matters is that children are updated * after their parents. So, this could be optimized by being smarter, and * updating all widgets concurrently, except for parents/children. + * + * A potential cheap way to improve this situation is to keep track of the + * depth of a component in the component tree. A root component has a depth of + * 1, then its children of 2 and so on... Then, we can update all components + * with the same depth in parallel. */ async __notifyComponents() { const id = ++this.id;