Components and state
Global state with stores
The cart shows up in the header and in the sidebar. Both need the same data, and they are not
neighbours in the HTML. A v-data settles it inside one stretch; for the whole page,
use a store.
Creating a store
V.store('cart', {
items: [],
total() {
return this.items.reduce((sum, item) => sum + item.price, 0);
},
add(product) {
this.items.push(product);
},
clear() {
this.items = [];
}
});
From then on it exists in $store, in any expression on the page, without having to
be passed anywhere:
<div>
<script>
V.store('cart', {
items: [],
total() { return this.items.reduce((s, i) => s + i.price, 0) },
add(p) { this.items.push(p) },
clear() { this.items = [] }
});
</script>
<div>
<button @click="$store.cart.add({ name: 'Mug', price: 39 })">
Add a mug
</button>
<button @click="$store.cart.clear()">Clear</button>
</div>
<p>Up here: { $store.cart.items.length } items</p>
<div v-data="{ someOtherScope: true }">
<p>In another scope, far away: $ { $store.cart.total() }</p>
</div>
</div>
Notice that the second paragraph is inside another v-data and still sees the cart.
There is no inheritance between them: the store belongs to no scope at all.
For derived values use a method, like the total() above. A property declared with
get is resolved exactly once, when the store is created, and does not follow the
changes. It is the most common mistake on this page.
Persistence
V.store('preferences', { theme: 'system', language: 'en-US' }, { persist: true });
The store is written to localStorage on every change and restored on the next load.
The default key is voodoo:store:<name>; pass a string in persist
to pick another, such as { persist: 'app:prefs' }. Functions are not written.
The store API
V.store('cart'); // gets the existing store back
V.store('cart', { ... }); // creates it, or updates the values keeping the reference
V.stores; // an object with every store, the same as $store
V.storeNames(); // ['cart', 'preferences']
V.removeStore('cart'); // removes it and stops the persistence
A store created after the page has loaded updates whoever was already on screen waiting for it. You do not have to guarantee that the store's file comes before the HTML that uses it.
The middle level: the root scope
Between the v-data of a block and a named store sits the root scope, which holds for
the whole page without becoming an object with a name of its own:
V.data({ user: null, version: '2.1' });
It shows up as $root in any expression, and loose identifiers fall through to it
when they exist in no scope above. It suits what is unique and has no behaviour: the signed-in
user, the system version, a setting that came from the server.
The event bus
For when what you need is not to share a value, but to announce that something happened:
const off = V.on('order:created', (order) => V.toast.success(`Order ${order.id} created`));
V.once('app:ready', () => console.log('only the first time'));
V.emit('order:created', { id: 42 });
off(); // cancels one subscription
V.off('order:created'); // cancels every subscription to that event
In the HTML, the equivalent is $dispatch, which fires a CustomEvent up
the tree — good for conversations between parts that are close by, while the bus is for parts
that do not know each other.
Which one to use
| Situation | Choice |
|---|---|
| State of one block of HTML | v-data |
| A value used by the whole page | V.data() |
| Cart, signed-in user, preferences | V.store() |
| Has to survive a reload | v-persist, or a store with persist |
| Has to follow other tabs | v-sync |
| Needs undo | v-history |
| A single text field | v-storage |
The last four rows have a page of their own, with the details of key, limit and behaviour: state that persists, syncs and undoes.