Skip to content

Store ​

A Store is a headless Rust/WASM capability. It owns an instance-scoped state machine and exposes a snapshot, subscriptions, declared actions, and disposal. It does not own a DOM subtree; the host application remains responsible for rendering the snapshot.

Basic usage ​

Declare the public role and keep the Rust implementation in an ordinary .rs module:

rust
#[voo::store]
impl Cart {
    #[voo::snapshot]
    pub fn snapshot(&self) -> CartSnapshot { /* ... */ }

    #[voo::action]
    pub fn add(&mut self, quantity: u32) { /* ... */ }
}

The generated module exposes an independent store instance and a framework convenience primitive. Vue, React, Solid, and Svelte receive the same generated names and fields. For a Cart store, the exports are createCartStore() and useCart():

vue
<script setup lang="ts">
import { useCart } from "./Cart.rs";

const { state, add } = useCart();
</script>

<template>
  <button type="button" @click="add(1)">Store {{ state?.count ?? 0 }}</button>
</template>
tsx
import { useCart } from "./Cart.rs";

export function CartButton() {
  const { state, add } = useCart();
  return <button onClick={() => add(1)}>Store {state?.count ?? 0}</button>;
}

Solid keeps the same generated name but exposes state as an accessor:

tsx
import { useCart } from "./Cart.rs";

export function CartButton() {
  const { state, add } = useCart();
  return <button onClick={() => add(1)}>Store {state()?.count ?? 0}</button>;
}

Svelte exposes state as a Readable, with template auto-subscription through $state:

svelte
<script>
  import { useCart } from "./Cart.rs";
  const { state, add } = useCart();
</script>

<button onclick={() => add(1)}>Store {$state?.count ?? 0}</button>

The generated entry name follows the Rust Store type, not the file name: ShoppingCart generates useShoppingCart. Vue exposes state as a reactive Ref (automatically unwrapped in templates), React exposes the current snapshot value, Solid exposes an Accessor, and Svelte exposes a Readable. undefined means that the asynchronous WASM Store is not ready yet. The generated names, actions, ownership, and error contract are otherwise aligned.

Internally, generation stops at one framework-neutral Store bridge containing the factory and declared actions. Each adapter wraps that bridge with its native reactivity and lifecycle primitives. This keeps framework branching out of the Rust/WASM generator without forcing Vue, React, Solid, and Svelte into an unnatural shared reactive container.

The lower-level useVooyaStore exports remain available from the framework adapters for custom integrations, shared instances, and adapter authors. They are not the primary API for ordinary application code.

The current ABI creates state from Rust's Default implementation. Constructor props and async actions are outside ABI v1. Snapshot fields, action arguments, and return values must use the shared owned ABI mapping. See the API reference for adapter options and the Rust authoring guide for the complete role syntax.

Contract ​

PartDirectionPurposeCurrent boundary
SnapshotRust → HostRead current serializable stateCached output is published through the store subscription
SubscriptionRust → HostNotify the adapter after a snapshot changeEvery subscription is disposable
ActionHost → RustRun a declared synchronous state transitionActions are explicit; async actions are not ABI v1
DisposeHost → RustRelease listeners and owned resourcesThe owner must call it for shared instances

Stores are instance-scoped by default. A component or host service may own one store, or several consumers may share one when a separate owner controls its lifetime. Generated useName() entries own and dispose the instance they create; the lower-level Vue disposeOnUnmount option remains explicit for custom integrations. A store is not a global singleton by implication.

Why signals? ​

Signals are the Rust runtime primitive for a value that can change and notify owned work. They are useful inside a Store for local state and derived computation, while the public Store boundary stays deliberately smaller: snapshot, subscription, action, and dispose.

Runtime toolWhat it does todayWhy it matters for a Store
Signal<T>Synchronous get, set, and updateKeeps Rust-owned state explicit and type-checked
effectRuns an explicitly subscribed callbackMakes the notification relationship visible
tracked_effectCollects signals read during a callback and replaces stale subscriptions on rerunSupports dynamic derived work without asking the author to maintain every subscription
batchCoalesces synchronous writes until the outer transaction completesPrevents observers from seeing intermediate combinations of one logical update
SignalSubscriptionUnsubscribes on dropGives branches, components, and stores deterministic cleanup

This split follows the discussion in Issue #60 and RFC 0002: static structure can be known by a compiler, but arbitrary Rust helpers and runtime branches need runtime dependency tracking. The current runtime provides explicit bindings and opt-in tracking. It does not promise that every Store action automatically becomes a fine-grained UI update, and it does not make a Store a second host renderer.

When to choose Store ​

Choose a Store when Rust should own reusable state or computation without owning markup: parsing state, a data model, validation, or a domain operation shared by several host views. Choose a Component when Rust also needs to create or update a local rendered tree. They can be composed, but they do not implicitly share ownership or lifecycle.

Vooya is a beta project. Check the compatibility matrix before relying on a path.