From 4415cc893218881381590efda945d7496801ebbd Mon Sep 17 00:00:00 2001 From: Samuel Degueldre Date: Fri, 15 Oct 2021 10:48:17 +0200 Subject: [PATCH] [REF] doc: remove references to router and store Owl 2 will not have a router or store implemented inside the library --- README.md | 1 - doc/miscellaneous/comparison.md | 91 +------- doc/readme.md | 2 - doc/reference/component.md | 2 +- doc/reference/concurrency_model.md | 6 +- doc/reference/content.md | 32 ++- doc/reference/event_bus.md | 2 - doc/reference/hooks.md | 28 --- doc/reference/router.md | 183 --------------- doc/reference/store.md | 349 ----------------------------- 10 files changed, 18 insertions(+), 678 deletions(-) delete mode 100644 doc/reference/router.md delete mode 100644 doc/reference/store.md diff --git a/README.md b/README.md index 269c6b1a..0bd8086d 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,6 @@ simple and consistent way. Owl's main features are: - a declarative component system, - a reactivity system based on hooks, - concurrent mode by default, -- a store and a frontend router Owl components are defined with ES6 classes, they use QWeb templates, an underlying virtual DOM, integrates beautifully with hooks, and the rendering is diff --git a/doc/miscellaneous/comparison.md b/doc/miscellaneous/comparison.md index db05c0f1..ffb6af5c 100644 --- a/doc/miscellaneous/comparison.md +++ b/doc/miscellaneous/comparison.md @@ -189,95 +189,6 @@ with a `Proxy`, which means that it is totally transparent to the developers. Adding new keys is supported. Once any part of the state has been changed, a rendering is scheduled in the next microtask tick (promise queue). -## State Management - -Managing the state of an application is a tricky issue. Many solutions have -been proposed these last few years. It also depends on the kind of application we -are talking about. A small application may not need much more than a simple -object to contain its state. - -However, there are some common solutions for React and Vue: redux and vuex. -Both of them are a centralized store that own the state, and they dictate how -the state can be mutated. - -**Redux** - -In Redux, the state is mutated by reducers. Reducers are functions -that modify the state by returning a different object: - -```javascript - ... - switch (action.type) { - case ADD_TODO: { - const { id, content } = action.payload; - return { - ...state, - allIds: [...state.allIds, id], - byIds: { - ...state.byIds, - [id]: { - content, - completed: false - } - } - }; - } -``` - -This is a little bit awkward to write, but this allows the component system to -check if a part of the state was changed. This is exactly what is done by the -`connect` function: it create a _connected_ component, which is subscribed to -the state and triggers a rerender if some part of the state was modified. - -**VueX** - -VueX is based on a different principle: the state is mutated through -some special functions (the mutations), which modify the state in place: - -```javascript - function ({state}, payload) { - const { id, content } = payload; - const message = {id, content, completed: false}; - state.messages.push(message) - } -``` - -This is simpler, but there is a little bit more happening behind the scene: -each key from the state is silently replaced by getters and setters, and VueX -keeps track of who get data, and retrigger a render when it was changed. - -**Owl** - -Owl store is a little bit like a mix of redux and vuex: it has actions (but not -mutations), and like VueX, it keeps track of the state changes. However, it does -not notify a component when the state changes. Instead, components need to connect -to the store like in redux, with the `useStore` hook (see the [store documentation](../reference/store.md#connecting-a-component)). - -```javascript -const actions = { - increment({ state }, val) { - state.counter.value += val; - }, -}; - -const state = { - counter: { value: 0 }, -}; -const store = new owl.Store({ state, actions }); - -class Counter extends Component { - static template = xml` - `; - counter = useStore((state) => state.counter); - dispatch = useDispatch(); -} - -Counter.env.store = store; -const counter = new Counter(); -``` - ## Hooks [Hooks](https://reactjs.org/docs/hooks-intro.html#motivation) recently took over @@ -342,4 +253,4 @@ class Example extends Component { Since the Owl framework had hooks from early in its life, its main APIs are designed to be interacted with hooks from the start. For example, the -`Context` and `Store` abstractions. +`Context` abstraction. diff --git a/doc/readme.md b/doc/readme.md index cf53639f..3a9914f3 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -35,8 +35,6 @@ provided by Owl. - [Props Validation](reference/props_validation.md) - [QWeb Templating Language](reference/qweb_templating_language.md) - [QWeb Engine](reference/qweb_engine.md) -- [Router](reference/router.md) -- [Store](reference/store.md) - [Slots](reference/slots.md) - [Tags](reference/tags.md) - [Utils](reference/utils.md) diff --git a/doc/reference/component.md b/doc/reference/component.md index 9e4c32b0..bde94d20 100644 --- a/doc/reference/component.md +++ b/doc/reference/component.md @@ -263,7 +263,7 @@ We explain here all the public methods of the `Component` class. Note that if a component is mounted, unmounted and remounted, it will be automatically re-rendered to ensure that changes in its state (or something - in the environment, or in the store, or ...) will be taken into account. + in the environment) will be taken into account. If a component is mounted inside an element or a fragment which is not in the DOM, then it will be rendered fully, but not active: the `mounted` hooks will diff --git a/doc/reference/concurrency_model.md b/doc/reference/concurrency_model.md index 22a5c3ab..3d081297 100644 --- a/doc/reference/concurrency_model.md +++ b/doc/reference/concurrency_model.md @@ -158,12 +158,10 @@ There are two different common problems with Owl asynchronous rendering model: Here are a few tips on how to work with asynchronous components: 1. Minimize the use of asynchronous components! -2. Maybe move the asynchronous logic in a store, which then triggers (mostly) - synchronous renderings -3. Lazy loading external libraries is a good use case for async rendering. This +2. Lazy loading external libraries is a good use case for async rendering. This is mostly fine, because we can assume that it will only takes a fraction of a second, and only once (see [`owl.utils.loadJS`](utils.md#loadjs)) -4. For all the other cases, the [`AsyncRoot`](misc.md#asyncroot) component is there to help you. When +3. For all the other cases, the [`AsyncRoot`](misc.md#asyncroot) component is there to help you. When this component is met, a new rendering sub tree is created, such that the rendering of that component (and its children) is not tied to the rendering of the rest of the interface. It can diff --git a/doc/reference/content.md b/doc/reference/content.md index 6e5cb4d1..dd3b5037 100644 --- a/doc/reference/content.md +++ b/doc/reference/content.md @@ -11,30 +11,26 @@ browser Component misc Context AsyncRoot QWeb Portal -mount router -Store Link -useState RouteComponent -config Router - mode -core tags - EventBus css - Observer xml -hooks utils - onWillStart debounce - onMounted escape - onWillUpdateProps loadJS - onWillPatch loadFile - onPatched shallowEqual - onWillUnmount whenReady +mount +useState tags +config css + mode xml +core utils + EventBus debounce + Observer escape +hooks loadJS + onWillStart loadFile + onMounted shallowEqual + onWillUpdateProps whenReady + onWillPatch + onPatched + onWillUnmount useContext useState useRef useComponent useEnv useSubEnv - useStore - useDispatch - useGetters ``` Note that for convenience, the `useState` hook is also exported at the root of the `owl` object. diff --git a/doc/reference/event_bus.md b/doc/reference/event_bus.md index 25dfb6d0..88b25c41 100644 --- a/doc/reference/event_bus.md +++ b/doc/reference/event_bus.md @@ -23,5 +23,3 @@ Its API is: | `off(eventType, owner)` | remove all listeners for an owner | | `trigger(eventType, ...args)` | trigger an event | | `clear` | remove all subscriptions | - -Note that the [`Store`](store.md) is an example of an `EventBus`. diff --git a/doc/reference/hooks.md b/doc/reference/hooks.md index c27fd56b..622cb005 100644 --- a/doc/reference/hooks.md +++ b/doc/reference/hooks.md @@ -18,9 +18,6 @@ - [`useRef`](#useref) - [`useSubEnv`](#usesubenv) - [`useExternalListener`](#useexternallistener) - - [`useStore`](#usestore) - - [`useDispatch`](#usedispatch) - - [`useGetters`](#usegetters) - [`useComponent`](#usecomponent) - [`useEnv`](#useenv) - [Making customized hooks](#making-customized-hooks) @@ -375,31 +372,6 @@ to be closed: useExternalListener(window, "click", this.closeMenu); ``` -### `useStore` - -The `useStore` hook is the entry point for a component to connect to the store. -See the [store documentation](store.md) for more information. - -### `useDispatch` - -The `useDispatch` hook is the way for components to get a reference to the store -`dispatch` function. See the [store documentation](store.md) for more information. - -### `useGetters` - -The `useGetters` hook is the way for components to get a reference to the store -getters. See the [store documentation](store.md) for more information. - -### `useComponent` - -The `useComponent` hook is useful as a building block for some customized hooks, -that may need a reference to the component calling them. - -### `useEnv` - -The `useEnv` hook is useful as a building block for some customized hooks, -that may need a reference to the env of the component calling them. - ### Making customized hooks Hooks are a wonderful way to organize the code of a complex component by feature diff --git a/doc/reference/router.md b/doc/reference/router.md deleted file mode 100644 index 352b2646..00000000 --- a/doc/reference/router.md +++ /dev/null @@ -1,183 +0,0 @@ -# 🦉 Router 🦉 - -Remove? - -## Content - -- [Overview](#overview) -- [Example](#example) -- [Reference](#reference) - - [Route Definition](#route-definition) - - [Router](#router) - - [Navigation Guards](#navigation-guards) - - [RouteComponent](#routecomponent) - - [Link](#link) - -## Overview - -It is often useful to organize an application around urls. If the application is -a single page application, then we need a way to manage those urls in the browser. -This is why there are many different routers for different frameworks. A generic -router can do the job just fine, but a specialized router for Owl can give a -better developer experience. - -The Owl router support the following features: - -- `history` or `hash` mode -- declarative routes -- route redirection -- navigation guards -- parameterized routes -- a `` component -- a `` component - -Note that it is still in early stage of developments, and there are probably -still some issues. - -## Example - -To use the Owl router, there are some steps that needs to be done: - -- declare some routes -- create a router -- add it to the environment - -```js -async function protectRoute({ env, to }) { - if (!env.session.authUser) { - env.session.setNextRoute(to.name); - return { to: "SIGN_IN" }; - } - return true; -} - -export const ROUTES = [ - { name: "LANDING", path: "/", component: Landing }, - { name: "TASK", path: "/tasks/{{id}}", component: Task }, - { name: "SIGN_UP", path: "/signup", component: SignUp }, - { name: "SIGN_IN", path: "/signin", component: SignIn }, - { name: "ADMIN", path: "/admin", component: Admin, beforeRouteEnter: protectRoute }, - { name: "ACCOUNT", path: "/account", component: Account, beforeRouteEnter: protectRoute }, - { name: "UNKNOWN", path: "*", redirect: { to: "LANDING" } } -]; - -function makeEnvironment() { - ... - const env = { qweb }; - env.session = new Session(env); - env.router = new owl.router.Router(env, ROUTES); - await env.router.start(); - return env; -} - -App.env = makeEnvironment(); -// create root component here -``` - -Notice that the router needs to be started. This is an asynchronous operation -because it needs to apply the potential navigation guards on the current route -(which may or may not mean that the application is redirected to another route). - -## Reference - -### Route definition - -A route need to be defined as an object with the following keys: - -- `name` (optional): a (unique) string useful to identify the current route. If not - given, it will be assigned an automatic name, -- `path`: a string describing the url. It can be static: `/admin` or dynamic: `/users/{{id}}`. - It also can be `*`, to catch all remaining routes. -- `component` (optional): an Owl component that will be used by the `t-routecomponent` - directive if the route is active -- `redirect` (optional): should be destination object (with optional keys `path`, `to` and `params`) if given, the application will be redirected to the destination whenever we match this route -- `beforeRouteEnter`: defines a [navigation guard](#navigation-guards). - -### `Router` - -The `Router` constructor takes three arguments: - -- `env`: a valid environment, -- a list of routes, -- an optional object (with the only key `mode` which can be `history` (default - value) or `hash`). - -`history` will use the browser [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) as the mechanism to manage URL.\ -Example: `https://yourdomain.tld/my_custom_route`.\ -For this mechanism to work, you need a way to configure your web server accordingly. - -`hash` will manipulate the hash of the URL.\ -Example: `https://yourdomain.tld/index.html#/my_custom_route`. - -```js -const ROUTES = [...]; -const router = new owl.router.Router(env, ROUTES, {mode: 'history'}); -``` - -Note that the route are defined in a list, and the order matters: the router -tries to find a match by going down the list. - -The router needs to be added to the environment in the `router` sub key. - -Once a router is created, it needs to be started. This is necessary to initialize -its current state to the current URL (and also, to potentially apply any -navigation guards and/or redirecting). - -```js -await router.start(); -``` - -Once started, the router will keep track of the current url and reflect its -value in two keys: - -- `router.currentRoute` -- `router.currentParams` - -The router also has a `navigate` method, useful to programmatically change the -application to another state (and the url): - -```js -router.navigate({ to: "USER", params: { id: 51 } }); -``` - -### Navigation Guards - -Navigation guards are very useful to be able to execute some business logic/ -perform some actions or redirect to other routes whenever the application is -entering a new route. For example, the following guard checks if there is an -authenticated user, and if it is not the case, redirect to the sign in route. - -```js -async function protectRoute({ env, to }) { - if (!env.session.authUser) { - env.session.setNextRoute(to.name); - return { to: "SIGN_IN" }; - } - return true; -} -``` - -A navigation guard is a function that returns a promise, which either resolves -to `true` (the navigation is accepted), or to another destination object. - -### `RouteComponent` - -The `RouteComponent` component directs Owl to render the component associated -to the currently active route (if any): - -```xml -
- - -
-``` - -### `Link` - -The `Link` component is a Owl component which render as a `` tag with any -content. It will compute the proper href from its props, and allow Owl to -properly navigate to a given url if clicked on it. - -```xml -Home -``` diff --git a/doc/reference/store.md b/doc/reference/store.md deleted file mode 100644 index a94553c9..00000000 --- a/doc/reference/store.md +++ /dev/null @@ -1,349 +0,0 @@ -# 🦉 Store 🦉 - -## Content - -- [Overview](#overview) -- [Example](#example) -- [Reference](#reference) - - [Store](#store) - - [Actions](#actions) - - [Getters](#getters) - - [Connecting a Component](#connecting-a-component) - - [`useStore`](#usestore) - - [`useDispatch`](#usedispatch) - - [`useGetters`](#usegetters) - - [Semantics](#semantics) - - [Good Practices](#good-practices) - -## Overview - -Managing the state in an application is not an easy task. In some cases, the -state of an application can be part of the component tree, in a natural way. -However, there are situations where some parts of the state need to be displayed -in various parts of the user interface, and then, it is not obvious which -component should own which part of the state. - -Owl's solution to this issue is a centralized store. It is a class that owns -some (or all) state, and lets the developer update it in a structured way, with -`actions`. Owl components can then connect to the store to read their relevant -state, and they will be rerendered if the state is updated. - -Note: Owl store is inspired by React Redux and VueX. - -## Example - -Here is what a simple store looks like: - -```js -const actions = { - addTodo({ state }, message) { - state.todos.push({ - id: state.nextId++, - message, - isCompleted: false, - }); - }, -}; - -const state = { - todos: [], - nextId: 1, -}; - -const store = new owl.Store({ state, actions }); -store.on("update", null, () => console.log(store.state)); - -// updating the state -store.dispatch("addTodo", "fix all bugs"); -``` - -This example shows how a store can be defined and used. Note that in most cases, -actions will be dispatched by connected components. - -## Reference - -### `Store` - -The store is a simple [`owl.EventBus`](event_bus.md) that triggers `update` events -whenever its state is changed. Note that these events are triggered only after a -microtask tick, so only one event will be triggered for any number of state changes in a -call stack. - -Also, it is important to mention that the state is observed (with an `owl.Observer`), -which is the reason why it is able to know if it was changed. See the -[Observer](observer.md)'s documentation for more details. - -The `Store` class is quite small. It has two public methods: - -- its constructor -- `dispatch` - -The constructor takes a configuration object with four (optional) keys: - -- the initial state -- the actions -- the getters -- the environment - -```javascript -const config = { - state, - actions, - getters, - env, -}; -const store = new Store(config); -``` - -### Actions - -Actions are used to coordinate state changes. It can be used for both synchronous -and asynchronous logic. - -```js -const actions = { - async login({ state }, info) { - state.loginState = "pending"; - try { - const loginInfo = await doSomeRPC("/login/", info); - state.loginState = loginInfo; - } catch (e) { - state.loginState = "error"; - } - }, -}; -``` - -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. - -```js -store.dispatch("login", someInfo); -``` - -Note that anything returned by an action will also be returned by the `dispatch` -call. - -Also, it is important to be aware that we need to be careful with asynchronous -logic. Each state change will potentially trigger a rerendering, so we need to -make sure that we do not have a partially corrupted state. Here is an example that -is likely not a good idea: - -```javascript -const actions = { - async fetchSomeData({ state }, recordId) { - state.recordId = recordId; - const data = await doSomeRPC("/read/", recordId); - state.recordData = data; - }, -}; -``` - -In the previous example, there is a period of time in which the state has a -`recordId` which does not correspond to the `recordData`. It is more likely that -we want an atomic update: updating the `recordId` at the same time as the `recordData` -values: - -```javascript -const actions = { - async fetchSomeData({ state }, recordId) { - const data = await doSomeRPC("/read/", recordId); - state.recordId = recordId; - state.recordData = data; - }, -}; -``` - -### Getters - -Usually, data contained in the store will be stored in a normalized way. For -example, - -```js -{ - posts: [{id: 11, authorId: 4, content: 'Greetings'}], - authors: [{id: 4, name: 'John'}] -} -``` - -However, the user interface will probably need some denormalized data like - -```js -{id: 11, author: {id: 4, name: 'John'}, content: 'Greetings'} -``` - -This is what `getters` are for: they give a centralized way to process and -transform the data contained in the store. - -```js -const getters = { - getPost({ state }, id) { - const post = state.posts.find((p) => p.id === id); - const author = state.authors.find((a) => a.id === post.id); - return { - id, - author, - content: post.content, - }; - }, -}; - -// somewhere else -const post = store.getters.getPost(id); -``` - -Getters take _at most_ one argument. - -Note that getters are not cached. - -### Connecting a Component - -At some point, we need a way to interact with the store from a component. This -means that the component needs a reference to the store. By default, it looks -for it in the `env.store` key. However, this can be configured with the `useStore` -hook. - -Every component-store interactions are done with the help of the three store hooks: - -- [`useStore`](#usestore) to subscribe a component to some part of the store state, -- [`useDispatch`](#usedispatch) to get a reference to a dispatch function, -- [`useGetters`](#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.value += val; - }, -}; - -const state = { - counter: { value: 0 }, -}; -const store = new owl.Store({ state, actions }); -``` - -To make it accessible to the complete application, we will put it in the -environment: - -```js -// in this example, the root component is App -App.env.store = store; -``` - -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 - -``` - -### `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 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` 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 (unless the `isEqual` option is defined, -then it will call it) and will update the component every time this check fails. - -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. - -Also, the return value from `useStore` is not supposed to be modified. The store -state should only be updated with actions. - -### `useDispatch` - -The `useDispatch` hook is useful when a component needs to be able to dispatch -actions. It takes an optional argument, which is a store. If not given, it will -use the store in the environment. - -Note that a component does not need to be connected in any other way to the store. -For example: - -```js -class DoSomethingButton extends Component { - static template = xml``; - dispatch = useDispatch(); -} -``` - -### `useGetters` - -The `useGetters` hook is useful when a component needs to be able to use the -getters defined in a store. It takes an optional argument, which is a store. If -not given, it will use the store in the environment. - -Note that a component does not need to be connected in any other way to the store. -For example: - -```js -class InfoButton extends Component { - static template = xml``; - getters = useGetters(); -} -``` - -### Semantics - -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 - before children), -- they are updated only if they are in the DOM, -- if a parent is asynchronous, the system will wait for it to complete its - update before updating other components, -- in general, updates are not coordinated. This is not a problem for synchronous - components, but if there are many asynchronous components, this could lead to - a situation where some part of the UI is updated and some other part of the UI is - not updated. - -### Good Practices - -- 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 `useStore` - call and a `Message` component could get the data of its own - message, -- 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.