diff --git a/README.md b/README.md index 1022b576..e72c91e5 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ Are you new to Owl? This is the place to start! - [Tutorial: create a TodoList application](doc/learning/tutorial_todoapp.md) - [How to start an Owl project](doc/learning/quick_start.md) - [How to test Components](doc/learning/how_to_test.md) +- [How to organize Owl code](doc/miscellaneous/organizing_code.md) ### Reference diff --git a/doc/miscellaneous/organizing_code.md b/doc/miscellaneous/organizing_code.md new file mode 100644 index 00000000..05926bae --- /dev/null +++ b/doc/miscellaneous/organizing_code.md @@ -0,0 +1,380 @@ +# 🦉 Organising Owl code 🦉 + +## Content + +- [Overview](#overview) +- [Reactivity](#reactivity) +- [Asynchrony](#asynchrony) +- [Basic design: Components and Props](#basic-design-components-and-props) +- [More advanced design: using the environment](#more-advanced-design-using-the-environment) +- [Even more advanced: completely separating model management](#even-more-advanced-completely-separating-model-management) +- [Special case: bypassing reactivity](#special-case-bypassing-reactivity) + +## Overview + +The topic of this document is to discuss how one could organize an Owl +application/component/feature/codebase. Clearly, good practices for a system depends +on its properties/semantics. For Owl 2.x applications, the most important +ideas to have in mind are the following: + +- Owl provides a way to structure a component tree, +- communication between a parent and a child component is mostly done with props + (and callback props to go from child to parent), +- there is an additional communication channel: information can be propagated + through the environment, +- Owl implements a reactivity system, which tracks each components and values, + so that it is able to only rerender the relevant subset of components, +- asynchrony: owl components can delay renderings with `onWillStart`/`onWillUpdateProps` +- renderings are batched (10 calls to render in the same call stack will only + result in 1 actual rendering) + +## Reactivity + +With the reactivity system, each value created with the `useState` or the `reactive` +function is a proxy that allows Owl to trask which keys/values have been read +by which components. This means that Owl can then only update the components that +are impacted by a state change. + +There are two important reactivity primitives: + +- `useState`: a hook to turn a value into a reactive value, linked to a component + (so, each time the value is updated, the component is rerendered) +- `reactive`: a function to create a "standalone" reactive value + +Also, it is good to know that one can call `useState` on a reactive value: this will +create another reactive value, connected to the same source, but linked to a +different component. + +Let us consider the following component tree: + +```mermaid +graph TD + A-->B + A-->C + B-->D + B-->E + B-->F + C-->G + C-->H +``` + +It is important to know how renderings are applied by Owl: + +- if a render is initiated in A, it will render its template +- whenever it encounter a child component (here, B and C), it will shallow + compare the before and after props. If they are different, Owl will render the + corresponding child component, otherwise it will stop. +- each props object received by a (child) component is turned into a reactive + object. + +Each (non primitive) value given by `A` to `B` and `C` will be transformed into +a reactive object. +basically, owl does a useState on props at each level, and compare +shallowly values + +Notes: + +- each component can be independently rendered (so, for example, only B and D) +- one can create reactive objects (just like useState, but not linked to any + component) with the `reactive` function +- escape hatch: one can mark an object as "raw" (non reactive) with `markRaw`, and get the target + object (with `toRaw`) +- note that it is bad practice to keep reference to the raw object: doing so means + that some => may miss + rendering + +- note: reactivity is not free. It may be expensive for some large objects. + +## Asynchrony + +explain: onWillStart and onWillUpdateProps are async => can delay render + +good usecase for lazyloading libs + +note: most of the time, update should be done in an atomic way: + +```js +// bad => will cause extra rendering, possibly corrupt +this.state.something = someValue; +const otherValue = await this.fetchSomething(); +this.state.otherValue = otherValue; +``` + +```js +// good +const otherValue = await this.fetchSomething(); +this.state.something = someValue; +this.state.otherValue = otherValue; +``` + +```js +// bad => will cause extra rendering, possibly corrupt +const someValue = getsomeValue(); +const otherValue = await this.fetchSomething(); +Object.assign(this.state, { + someValue, + otherValue, +}); +``` + +other note: minimize async stuff + +```js +setup() { + // strategy 1: have a loading screen + this.state = useState({loading: true}); +} +``` + +```xml + + +... +``` + +// other strategy: not display a loading indicator, and update the UI atomically + +```js +setup() { + onWillStart(() => this.loadData()); + onWillUpdateProps(() => this.updateData()); +} +``` + +## Baseline design: Components and props + +Here is what I suppose many Owl systems will look like: `useState` in some components, +state is propagated down the tree with props, and potentially callbacks as well +(to communicate from child to parent) + + A + +/ \ + B C +/ | \ | \ +D E F G H + +```js +// in A (and maybe some/all other components) +this.state = useState(...); +``` + +```xml + +
+ + + + +
+``` + +Notice the `.bind` suffix here, very convenient. + +```xml + + +``` + +With reactivity, any update to the state will only rerender the affected components. +So, if some code in `A` update the value of `some_field` (without reading it!), +only B will be rerendered. + +## More advanced design: passing stuff through the environment + +It may be tedious/impractical to pass props all along the tree when we have a deep +component tree. + +One can then use the environment to communicate to children. Only issue with that +is that stuff in the environment bypass the reactivity system: if it comes from +a `useState` in `A` => only `A` will react to state changes, not the children that +reads it. + +To solve that, one need to use `useState` on it: + +```js +// in root component A +class A extends Component { + setup() { + this.posModel = { + state: useState(this.state), + updateSomeValue: () => this.updateSomeValue(), + doSomethingElse: () => this.doSomethingElse(), + }; + useSubEnv({ posModel: this.posModel }); + } +} + +// in some child component D +class D extends Component { + setup() { + // this one is wrong! this.posModel.state is stated + this.posModel = this.env.posModel; + + // this.posModel.state === the one in A, linked to the component A + + // this one is ok! + this.posModel = useState(this.env.posModel); + } +} +``` + +Note that we can slightly simplify the use of the model by a hook: + +```js +function usePosModel() { + const env = useEnv(); + const model = env.posModel; + return useState(model); +} +``` + +This is not much simpler, but it makes sure that one does not forget to call +`useState` on the model from the environment. + +## Even more advanced: completely separating model management + +The baseline design above it nice for many usecases, but we sometimes may want +to separate the "model" management from the UI, especially as the model is doing +more complicated stuff. Useful to code some complex transitions. + +Here is how we could use the environment and reactive object to organize code: + +```js +// in my_model.js +class MyModel { + ... + constructor() { + this.complicatedCacheObject = {}; + + markRaw(this.complicatedCacheObject); + } + + async load(...) { + + } + + async update(...) { + + } + + async doSomeComplexThing() { + this.loading = true; + + // coordinate here some thing + // fetch stuff... + + this.loading = false; + this.data = ...; + ... + } + ... +} + +// in root component A: + +class A extends Component { + setup() { + const model = useState(new MyModel(...)); + useSubEnv({ model }); + } +} + +// in some child component: +class D extends Component { + setup() { + // notice the useState: this is necessary to bind model changes to the + // component D + this.model = useState(this.env.model); + } +} + +``` + +Notes: + +- model could be created elsewhere, in a service, in the start code, ... +- one may want to mark some internal stuff as 'raw'... +- if created elsewhere, it should probably be done with `reactive` +- the call to `reactive` could be done in the model constructor directly: + + ```js + class MyModel { + constructor() { + ... + return reactive(this); + } + } + ``` + +- one could slightly simplify the model use with a hook: + +```js +function useModel() { + const env = useEnv(); + const model = env.model; + return useState(model); +} +``` + +This can be simply used like this in a child component: + +```js +setup() { + this.model = useModel(); +} +``` + +## Special case: bypassing reactivity + +Sometimes, reactivity is not what we want. For example, the o_spreadsheet +library has a huge extremely complex model, and each state transition may +potentially update the full UI, so it needs to be fast anyway, and reactivity +has a cost that may outweight the benefits. + +The strategy here is: + +- mark the model as raw (not necessary in all cases, but potentially important + if model is passed through some props) +- find a way to be notified on update change (for example, by an event triggered + by an event bus) +- perform a deep rendering: render(true), at the root component. + +Here is how one could organize the code then: + +```js +class MainModel extends EventBus{ + constructor() { + markRaw(this); // make sure this object will never be turned into a reactive + } + + doSomething() { + ... + // need to notify the outside world that a change occurred + this.trigger("UPDATE"); + } + ... +} + +// in root component: +class A extends Component { + setup() { + const model = new MainModel(); + + // notice the 'render(true)', to do a deep render + model.addEventListener("UPDATE", () => this.render(true)); + useSubEnv({ model }); + } +} + +// in child components: +class E extends Component { + setup() { + this.model = this.env.model; // not really necessary, but maybe nice + } + + onClick() { + this.model.doSomething(); + } +} +```