Vue
Vue bindings for Stan (@rkrupinski/stan/vue).
Reactive inputs
All value-returning composables (useStan, useStanValue, useStanValueAsync, useStanRefresh, useStanReset) accept either a plain Scoped<State<T>> or a Ref<Scoped<State<T>>>. Wrap family calls in computed to feed a reactive parameter - the composable will re-subscribe automatically whenever the underlying scoped state changes.
<script setup lang="ts">
import { computed } from 'vue';
import { selectorFamily } from '@rkrupinski/stan';
import { useStanValueAsync } from '@rkrupinski/stan/vue';
const user = selectorFamily<Promise<User>, string>(
userId => () => loadUser(userId),
);
const props = defineProps<{ userId: string }>();
const data = useStanValueAsync(computed(() => user(props.userId)));
</script>
useStan
Returns a WritableComputedRef for WritableState<T>. Reading .value subscribes to state changes; assigning to .value writes through to the store. Works with v-model.
Works with: atom, atomFamily
const useStan: <T>(
scopedState: Scoped<WritableState<T>> | Ref<Scoped<WritableState<T>>>,
) => WritableComputedRef<T>;
useStan does not work with ReadonlyState<T>.
Example:
<script setup lang="ts">
import { atom } from '@rkrupinski/stan';
import { useStan } from '@rkrupinski/stan/vue';
const myAtom = atom(42);
const count = useStan(myAtom);
</script>
<template>
<pre>{{ count }}</pre>
<button @click="count++">Add one</button>
</template>
useStanValue
Returns a readonly Ref for ReadonlyState<T>. It will also subscribe to (and unsubscribe from) state changes.
Works with: selector, selectorFamily
const useStanValue: <T>(
scopedState: Scoped<ReadonlyState<T>> | Ref<Scoped<ReadonlyState<T>>>,
) => Readonly<Ref<T>>;
For WritableState<T> (atoms), use useStan instead.
Example:
<script setup lang="ts">
import { atom, selector } from '@rkrupinski/stan';
import { useStanValue } from '@rkrupinski/stan/vue';
const myAtom = atom(42);
const doubled = selector(({ get }) => get(myAtom) * 2);
const value = useStanValue(doubled);
</script>
<template>
<p>{{ value }}</p>
</template>
useStanValueAsync
Wraps ReadonlyState<T> whose type extends PromiseLike<any> in a readonly Ref of a special union type:
type AsyncValue<T, E = unknown> =
| { type: 'loading' }
| { type: 'ready'; value: T }
| { type: 'error'; reason: E };
Works with: selector, selectorFamily
const useStanValueAsync: <T, E = unknown>(
scopedState:
| Scoped<ReadonlyState<PromiseLike<T>>>
| Ref<Scoped<ReadonlyState<PromiseLike<T>>>>,
) => Readonly<Ref<AsyncValue<T, E>>>;
useStanValueAsync is specifically designed to work only with asynchronous state (State<PromiseLike<any>>). Failing to comply may result in unpredictable behavior or errors.
Example:
<script setup lang="ts">
import { selector } from '@rkrupinski/stan';
import { useStanValueAsync } from '@rkrupinski/stan/vue';
const luke = selector(async ({ signal }) => {
const res = await fetch('https://www.swapi.tech/api/people/1', { signal });
return res.json();
});
const result = useStanValueAsync(luke);
</script>
<template>
<p v-if="result.type === 'loading'">Loading…</p>
<p v-else-if="result.type === 'error'">{{ String(result.reason) }}</p>
<pre v-else>{{ JSON.stringify(result.value, null, 2) }}</pre>
</template>
useStanRefresh
Wraps refresh and returns a function that refreshes ReadonlyState<T>.
Works with: selector, selectorFamily
const useStanRefresh: <T>(
scopedState: Scoped<ReadonlyState<T>> | Ref<Scoped<ReadonlyState<T>>>,
) => () => void;
Example:
<script setup lang="ts">
import { computed } from 'vue';
import { selectorFamily } from '@rkrupinski/stan';
import { useStanRefresh } from '@rkrupinski/stan/vue';
const props = defineProps<{ userId: string }>();
const users = selectorFamily<Promise<User>, string>(id => () => getUser(id));
const refresh = useStanRefresh(computed(() => users(props.userId)));
</script>
<template>
<UserDetails :user-id="userId" />
<button @click="refresh">Refresh</button>
</template>
useStanReset
Wraps reset and returns a function that resets WritableState<T>.
Works with: atom, atomFamily
const useStanReset: <T>(
scopedState: Scoped<WritableState<T>> | Ref<Scoped<WritableState<T>>>,
) => () => void;
Example:
<script setup lang="ts">
import { atom } from '@rkrupinski/stan';
import { useStan, useStanReset } from '@rkrupinski/stan/vue';
const counter = atom(0);
const count = useStan(counter);
const resetCount = useStanReset(counter);
</script>
<template>
<pre>{{ count }}</pre>
<button @click="count++">Increment</button>
<button @click="resetCount">Reset</button>
</template>
useStanCallback
Returns a callback with access to helper functions for interacting with Stan state (setting, refreshing, etc.).
const useStanCallback: <A extends unknown[], R>(
factory: (helpers: StanCallbackHelpers) => (...args: A) => R,
) => (...args: A) => R;
factory– A curried callback function, where:helpers– State helpers:get– A function for gettingState<T>value, with the following signature:<T>(scopedState: Scoped<State<T>>) => T;set– A function for settingWritableState<T>, with the following signature:<T>(scopedState: Scoped<WritableState<T>>,valueOrUpdater: T | ((currentValue: T) => T),) => voidreset– A function for resettingWritableState<T>, with the following signature:<T>(scopedState: Scoped<WritableState<T>>) => voidrefresh– A function for refreshingReadonlyState<T>, with the following signature:<T>(scopedState: Scoped<ReadonlyState<T>>) => void
Unlike React's useStanCallback, the Vue composable takes no deps array. A Vue component's setup() runs once, so the returned callback already captures the latest values via closure.
Example:
<script setup lang="ts">
import { selectorFamily } from '@rkrupinski/stan';
import { useStanCallback } from '@rkrupinski/stan/vue';
const user = selectorFamily<Promise<User>, string>(
userId => () => loadUser(userId),
);
const reloadUser = useStanCallback(({ refresh }) => (userId: string) => {
refresh(user(userId));
});
</script>
<template>
<ul>
<li v-for="{ id, name } in users" :key="id">
{{ name }}
<button @click="reloadUser(id)">Refresh</button>
</li>
</ul>
</template>
StanProvider
Stan, by default, operates in provider-less mode, using DEFAULT_STORE. However, if you need to supply a different store (e.g., for SSR) or switch stores dynamically, StanProvider comes in handy.
Using StanProvider creates an isolation boundary:
- No provider - composables use
DEFAULT_STORE StanProviderwithoutstoreprop - creates a fresh, isolated storeStanProviderwithstoreprop - uses the provided store
Composables track the current store through Vue's reactivity - changing StanProvider's :store prop re-subscribes them automatically. Use :key on StanProvider only when you actually want to tear down and recreate the subtree (e.g., to discard local component state alongside the store switch).
const StanProvider: DefineComponent<StanProviderProps>;
Props:
store?- AStoreinstance.
Example:
<script setup lang="ts">
import { makeStore } from '@rkrupinski/stan';
import { StanProvider } from '@rkrupinski/stan/vue';
const myStore = makeStore();
</script>
<template>
<StanProvider :store="myStore">
<Layout>
<RouterView />
</Layout>
</StanProvider>
</template>
provideStan
An alternative to wrapping your tree in StanProvider: call provideStan in a component's setup() to provide a store to descendants.
const provideStan: (store?: Store) => void;
Example:
<script setup lang="ts">
import { makeStore } from '@rkrupinski/stan';
import { provideStan } from '@rkrupinski/stan/vue';
const myStore = makeStore();
provideStan(myStore);
</script>
<template>
<Layout>
<RouterView />
</Layout>
</template>
useStanStore
In rare cases where you need to peek into the current Stan store injection, here's how you can do it.
const useStanStore: () => StanStoreInjection;
StanStoreInjection fields:
store- AComputedRefwrapping the currentStoreinstance.
useStanStore is a low-level API and should therefore be considered unstable.