A router component's $store wraps the Store: reading $store.getters.cart subscribes the component, and each change sets the getter's .value and calls the component's update(), which re-renders on the next animation frame. cart.f7 starts with const cart = $store.getters.cart; (and the same for cartTotal and cartCount), its template uses cart.value, and the root component's tab badge reads cartCount the same way. One dispatch from anywhere updates both, a frame later:
const badge = () => document.querySelector('.tabbar .badge')?.textContent || 'none';
const rows = () => document.querySelectorAll('#view-cart li.swipeout').length;
console.log(`before: badge ${badge()}, cart rows ${rows()}`);
app.store.dispatch('addToCart', 2);
console.log(`same tick: badge ${badge()}, cart rows ${rows()}`);
await new Promise((done) => setTimeout(done, 50));
console.log(`50 ms on: badge ${badge()}, cart rows ${rows()}`);before: badge none, cart rows 0 same tick: badge none, cart rows 0 50 ms on: badge 1, cart rows 1
Read getters at the top of the component, never inside a handler: each read adds a subscription that is never removed. The Cart tab's Clear handler used to read $store.getters.cartCount.value on every tap; counting getter runs per cart change gave 3, then 4, 5 and 6 after three taps, and a steady 4 once the read moved to the top. Outside components, subscribe with store.getters.cartCount.onUpdated(fn).