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.

A method, not a getter

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
The order of the scripts does not matter

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

SituationChoice
State of one block of HTMLv-data
A value used by the whole pageV.data()
Cart, signed-in user, preferencesV.store()
Has to survive a reloadv-persist, or a store with persist
Has to follow other tabsv-sync
Needs undov-history
A single text fieldv-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.