# Stan > Minimal, type-safe state management - **Repository**: https://github.com/rkrupinski/stan - **Documentation**: https://stan.party - **License**: MIT Stan is a minimal, type-safe state management library. It is **framework-agnostic** at its core, designed to be composable and extensible. While it includes first-class React and Vue bindings out of the box, the core logic depends on no specific framework. ## Core Concepts ### Mental Model Stan distinguishes between the *definition* of state and the *storage* of state. - **Atom**: A definition of a writable state unit. - **Selector**: A definition of a derived state unit (computed from atoms or other selectors). - **Store**: The container that holds the actual values. - **State**: The interface for interacting with a specific value in a specific store. ### Scoped State Primitives like `atom` and `selector` do not hold values themselves. They are factories (specifically, curried functions) that accept a `Store` and return a `State` object. ```typescript // Core type definition type Scoped = (store: Store) => T; ``` ### State Interfaces All state in Stan follows a common interface: ```typescript interface State { key: string; get(): T; subscribe(callback: (value: T) => void): () => void; } interface WritableState extends State { set(value: T | ((prev: T) => T)): void; } interface ReadonlyState extends State { // Can be refreshed but not set directly } ``` ## Core API ### `atom` Creates a writable state unit. Atoms have no dependencies and are the source of truth in the state graph. ```typescript function atom(initialValue: T, options?: AtomOptions): Scoped> ``` **Options:** - `tag`: A string (or string-returning function) for debugging purposes. - `effects`: Array of [Atom Effects](#atom-effects). - `areValuesEqual`: Custom equality check (default: `===`). ```typescript const countAtom = atom(0); const userAtom = atom({ name: 'John' }, { tag: 'user' }); ``` ### `atomFamily` Returns a memoized function that maps parameters to `atom`s. Useful for collections. ```typescript function atomFamily( initialValue: T | ((param: P) => T), options?: AtomFamilyOptions ): (param: P) => Scoped> ``` **Constraint**: `P` must be serializable (JSON-compatible). ### `selector` Creates a derived state unit. Selectors automatically track dependencies accessed via `get`. ```typescript function selector(fn: SelectorFn, options?: SelectorOptions): Scoped> ``` **Selector Function:** `({ get, signal }) => T | Promise` - `get`: Reads values from other atoms/selectors and subscribes to them. - `signal`: `AbortSignal` for cancelling async work. **Behavior**: - **Synchronous**: Re-evaluates immediately when dependencies change. - **Asynchronous**: Re-evaluates when dependencies change. The returned `Promise` is cached. ```typescript // Synchronous const doubleAtom = selector(({ get }) => get(countAtom) * 2); // Asynchronous const userQuery = selector(async ({ get, signal }) => { const res = await fetch('/api/user', { signal }); return res.json(); }); ``` ### `selectorFamily` Returns a memoized function that maps parameters to `selector`s. ```typescript function selectorFamily( fn: (param: P) => SelectorFn, options?: SelectorFamilyOptions

): (param: P) => Scoped> ``` **Options**: - `cachePolicy`: Configures internal cache behavior. - `{ type: 'keep-all' }` (default): Caches every instance. - `{ type: 'most-recent' }`: Keeps only the last used instance. - `{ type: 'lru', maxSize: number }`: Least Recently Used eviction. - Every variant additionally accepts an optional `ttl: number` (in milliseconds). The timer starts when an entry is added to the cache, and the expired entry is evicted lazily on the next access. While an entry has active subscribers (i.e. it's mounted), eviction is deferred - the entry remains stable until it is unreferenced again. ### `Store` The central state container. Required for Vanilla JS or advanced scoping (e.g., SSR). - `makeStore()`: Creates a new store instance. - `DEFAULT_STORE`: The default instance used by React bindings if no provider is present. ### `utils` - `reset(state)`: Resets a `WritableState` to its initial value. Notifies subscribers. - `refresh(state)`: Refreshes a `ReadonlyState`. - **Mounted**: Re-evaluates immediately. - **Unmounted**: Marks as dirty; re-evaluates on next access. ## React Integration (`@rkrupinski/stan/react`) Stan provides hooks that automatically handle store context. ### `useStan` Similar to `useState`. Returns `[value, setValue]`. ```tsx const [count, setCount] = useStan(countAtom); ``` ### `useStanValue` Reads a value and subscribes to updates. Works with atoms and selectors. ```tsx const double = useStanValue(doubleAtom); ``` ### `useSetStanValue` Returns a setter function for an atom. Does not subscribe to value changes (optimization). ```tsx const setCount = useSetStanValue(countAtom); ``` ### `useStanValueAsync` Specialized hook for `Promise`-based state. Wraps the promise state in a discriminated union for easy rendering. **Return Type**: ```typescript type AsyncValue = | { type: 'loading' } | { type: 'ready'; value: T } | { type: 'error'; reason: E }; ``` ```tsx const result = useStanValueAsync(userQuery); if (result.type === 'loading') return ; if (result.type === 'error') return ; return

{result.value.name}
; ``` ### `useStanRefresh` / `useStanReset` Hooks that return the `refresh` or `reset` functions bound to the specific state. ```tsx const refreshUser = useStanRefresh(userQuery); const resetCount = useStanReset(countAtom); ``` ### `useStanCallback` Create a callback with access to `get`, `set`, `reset`, and `refresh` without subscribing the component to state changes. ```typescript type StanCallbackHelpers = { get: (scopedState: Scoped>) => T; set: (scopedState: Scoped>, valueOrUpdater: T | ((currentValue: T) => T)) => void; reset: (scopedState: Scoped>) => void; refresh: (scopedState: Scoped>) => void; }; ``` ```typescript const cb = useStanCallback(({ get, set, reset, refresh }) => (arg) => { // Perform actions }); ``` ### `StanProvider` Used for Server-Side Rendering (SSR) to isolate state per request. ```tsx import { StanProvider } from '@rkrupinski/stan/react'; export default function App({ children }) { return {children}; // Uses a new store context } ``` ## Vue Integration (`@rkrupinski/stan/vue`) Stan provides composables for Vue 3 (peer dep: `vue >=3.4.0`). All composables bind to the store provided via `StanProvider` / `provideStan`, falling back to `DEFAULT_STORE`. Value-returning composables accept either a plain `Scoped>` or a `Ref>>`; wrap family calls in `computed` for reactive parameters. Bare getters (`() => family(param)`) are deliberately **not** accepted - `Scoped` is itself a function, so the two forms can't be disambiguated. ### `useStan` Returns a `WritableComputedRef` for writable state (atoms only). Reading `.value` subscribes; assigning writes through. Works with `v-model`. ```ts const useStan: ( scopedState: Scoped> | Ref>>, ) => WritableComputedRef; ``` ```vue ``` ### `useStanValue` Returns `Readonly>`. Works with atoms and selectors. ```ts const useStanValue: ( scopedState: Scoped> | Ref>>, ) => Readonly>; ``` Reactive usage: ```vue ``` ### `useStanValueAsync` Wraps a `PromiseLike` selector in a readonly `Ref` of the `AsyncValue` discriminated union defined above (`loading` / `ready` / `error`). ```ts const useStanValueAsync: ( scopedState: | Scoped>> | Ref>>>, ) => Readonly>>; ``` ### `useStanRefresh` / `useStanReset` Return bound `() => void` functions for the given state. Resolve the scoped state at call time, so they always target the current member when passed a ref. ```ts const useStanRefresh: ( scopedState: Scoped> | Ref>>, ) => () => void; const useStanReset: ( scopedState: Scoped> | Ref>>, ) => () => void; ``` ### `useStanCallback` Create a callback with access to `get`, `set`, `reset`, and `refresh` without subscribing the component to state changes. Unlike the React version, it takes no `deps` array - a Vue component's `setup()` runs once, so the returned callback captures the latest values via closure. ```ts const useStanCallback: ( factory: (helpers: StanCallbackHelpers) => (...args: A) => R, ) => (...args: A) => R; ``` ### `StanProvider` Vue component that provides a store to descendants. Without a `store` prop, creates a fresh isolated store. Changing the `:store` prop re-subscribes composables automatically through Vue's reactivity; use `:key` only when you want to tear down local component state alongside the swap. ```ts const StanProvider: DefineComponent<{ store?: Store }>; ``` ```vue ``` ### `provideStan` Alternative to `StanProvider`: call from a component's `setup()` to provide a store to descendants. ```ts const provideStan: (store?: Store) => void; ``` ### `useStanStore` Low-level (unstable) API returning the current Stan store injection - a `{ store: ComputedRef }`. ## Guides & Best Practices ### Type Safety Stan leverages TypeScript inference extensively. - Atoms infer type from initial value. - Selectors infer type from return value. - Families require explicit generic arguments for parameters if they cannot be inferred: `atomFamily(...)`. **Refining Types:** ```typescript const atomA = atom<42 | 43>(42); // WritableState<42 | 43> ``` ### Atom Effects Use effects for persistence, logging, or synchronization. Effects run in order. ```typescript type AtomEffect = (params: { init(value: T): void; // Set initial value (synchronous only) set: (valueOrUpdater: T | ((currentValue: T) => T)) => void; // Update value (supports updater fn) onSet(cb: (val: T) => void): void; // Subscribe to changes }) => void; ``` **Example (Persistence):** ```typescript const persistentAtom = atom(0, { effects: [ ({ onSet, set }) => { const saved = localStorage.getItem('count'); if (saved) set(Number(saved)); onSet(val => localStorage.setItem('count', String(val))); } ] }); ``` ### Caching - **Internal Cache**: Stan caches values of atoms and selectors. - **Eviction**: - Atoms: Cached until reset or updated. - Selectors: Cached until dependencies change. - Families: Controlled by `cachePolicy` (`keep-all`, `most-recent`, `lru`). Every variant additionally accepts an optional `ttl` (ms) for time-based eviction; both size- and time-based eviction are deferred while an entry is mounted. ### Param Serialization Keys for families are generated by serializing parameters. - **Supported**: JSON-serializable values (string, number, boolean, null, object, array). - **Behavior**: Keys are stable (object property order is normalized). ### Error Handling Selectors using `get()` to read dependencies do **not** throw if the dependency is in an error state. They simply propagate the error state or allow you to catch it if using async/await. ```typescript const safeSelector = selector(async ({ get }) => { try { return await get(asyncDep); } catch (e) { return 'fallback'; } }); ``` ## Debugging Stan comes with a dedicated [Chrome DevTools extension](https://chromewebstore.google.com/detail/stan-devtools/jioipgcofbmgbdfmdjjcmockkjkhagac) that lets you: - Dynamically switch between all active stores - Inspect and track the history of state changes - Search and filter state entries ### State tagging Tag your state for meaningful labels in DevTools (atoms and selectors both accept `tag`): ```typescript atom('all', { tag: 'View' }); selector(fn, { tag: 'Filtered todos' }); ``` For families, use a function tag (maps param → label): ```typescript const searchResults = selectorFamily, string>( phrase => () => getResults(phrase), { tag: phrase => `Search results for: ${phrase}` }, ); ``` ## Vanilla JS Usage Stan can be used without React by manually managing the `Store`. ```typescript import { atom, makeStore } from '@rkrupinski/stan'; const store = makeStore(); const count = atom(0); // Access state scoped to the store const countState = count(store); // Read console.log(countState.get()); // 0 // Write countState.set(1); // Subscribe const unsubscribe = countState.subscribe(val => console.log(val)); ```