Files
owl/doc/store.md
T

210 lines
5.4 KiB
Markdown
Raw Normal View History

# 🦉 Store 🦉
2019-05-10 10:49:14 +02:00
## Content
2019-05-10 10:49:14 +02:00
- [Overview](#overview)
- [Example](#example)
- [Reference](#reference)
- [Public API](#public-api)
- [Mutations](#mutations)
- [Actions](#actions)
- [Getters](#getters)
2019-06-14 15:24:04 +02:00
- [Connecting a Component](#connecting-a-component)
2019-05-10 10:49:14 +02:00
## 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 part 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 state, and let the developer update it in a structured way (through
mutations and actions). Owl components can then connect to the store, and will
be updated if necessary.
Note: Owl's store is inspired by React Redux and VueX.
## Example
Here is what a simple store looks like:
2019-05-10 10:49:14 +02:00
```js
const actions = {
2019-06-14 15:24:04 +02:00
addTodo({ commit }, message) {
commit("addTodo", message);
}
2019-05-10 10:49:14 +02:00
};
const mutations = {
2019-06-14 15:24:04 +02:00
addTodo({ state }, message) {
const todo = {
id: state.nextId++,
message,
isCompleted: false
};
state.todos.push(todo);
}
2019-05-10 10:49:14 +02:00
};
const state = {
2019-06-14 15:24:04 +02:00
todos: [],
nextId: 1
2019-05-10 10:49:14 +02:00
};
2019-06-14 15:24:04 +02:00
const store = new owl.Store({ state, actions, mutations });
store.on("update", () => console.log(store.state));
2019-05-10 10:49:14 +02:00
// updating the state
2019-06-14 15:24:04 +02:00
store.dispatch("addTodo", "fix all bugs");
2019-05-10 10:49:14 +02:00
```
## Reference
2019-06-07 15:47:04 +02:00
The store is a simple [`owl.EventBus`](event_bus.md) that triggers `update` events whenever its
2019-06-14 15:24:04 +02:00
state is changed. Note that these events are triggered only after a microtask
2019-05-10 10:49:14 +02:00
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`),
2019-06-14 15:24:04 +02:00
which is the reason why it is able to know if it was changed. This implies that
2019-05-10 10:49:14 +02:00
state changes need to be done carefully in some cases (adding a new key to an
2019-06-14 15:24:04 +02:00
object, or modifying an array with the `arr[i] = newValue` syntax). See the
2019-05-10 10:49:14 +02:00
[Observer](observer.md)'s documentation for more details.
### Public API
1. `constructor`
2. `commit`
3. `dispatch`
### Mutations
2019-06-14 15:24:04 +02:00
Mutations are the only way to modify the state. Changing the state outside a
mutation is not allowed (and should throw an error). Mutations are synchronous.
2019-05-10 10:49:14 +02:00
### Actions
2019-06-14 15:24:04 +02:00
Actions are used to coordinate state changes. It is also useful whenever some
asynchronous logic is necessary. For example, fetching data should be done
2019-05-10 10:49:14 +02:00
in an action.
```js
const actions = {
2019-06-14 15:24:04 +02:00
async login({ commit }) {
commit("setLoginState", "pending");
try {
const loginInfo = await doSomeRPC("/login/", "someinfo");
commit("setLoginState", loginInfo);
} catch {
commit("setLoginState", "error");
2019-05-10 10:49:14 +02:00
}
2019-06-14 15:24:04 +02:00
}
2019-05-10 10:49:14 +02:00
};
```
### 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 = {
2019-06-14 15:24:04 +02:00
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
};
}
2019-05-10 10:49:14 +02:00
};
2019-05-08 17:10:12 +02:00
// somewhere else
const post = store.getters.getPost(id);
2019-05-10 10:49:14 +02:00
```
2019-06-14 22:14:17 +02:00
Getters take *at most* one argument.
Note that getters are cached if they don't take any argument, or their argument
is a string or a number.
2019-06-14 15:24:04 +02:00
### Connecting a Component
2019-05-10 10:49:14 +02:00
By default, an Owl `Component` is not connected to any store. The `connect`
function is there to create sub Components that are connected versions of
Components.
```javascript
const actions = {
2019-06-14 15:24:04 +02:00
increment({ commit }) {
commit("increment", 1);
}
};
const mutations = {
2019-06-14 15:24:04 +02:00
increment({ state }, val) {
state.counter += val;
}
};
const state = {
2019-06-14 15:24:04 +02:00
counter: 0
};
2019-06-14 15:24:04 +02:00
const store = new owl.Store({ state, actions, mutations });
class Counter extends owl.Component {
2019-06-14 15:24:04 +02:00
increment() {
this.env.store.dispatch("increment");
}
}
function mapStoreToProps(state) {
2019-06-14 15:24:04 +02:00
return {
value: state.counter
};
}
2019-06-11 09:32:28 +02:00
const ConnectedCounter = owl.connect(Counter, mapStoreToProps);
const counter = new ConnectedCounter({ store, qweb });
```
2019-06-14 15:24:04 +02:00
```xml
<button t-name="Counter" t-on-click="increment">
Click Me! [<t t-esc="props.value"/>]
</button>
```
The arguments of `connect` are:
2019-06-14 15:24:04 +02:00
- `Counter`: an owl `Component` to connect
- `mapStoreToProps`: a function that extracts the `props` of the Component
from the `state` of the `Store` and returns them as a dict
- `options`: dictionary of optional parameters that may contain
- `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`: [only useful if no hashFunction is given] if false, only watch
for top level state changes (true by default)
2019-06-11 09:32:28 +02:00
The `connect` function returns a sub class of the given `Component` which is
connected to the `store`.