@molecule/app-state
Core interface · state · App (browser) · v1.0.3 · Apache-2.0
Global state management with stores
npm install @molecule/app-stateHow it works
@molecule/app-state is the state core interface on the app (browser) side: the API your app calls, with no vendor inside.
Choose the implementation by bonding one of its 3 providers: @molecule/app-state-jotai, @molecule/app-state-redux, @molecule/app-state-zustand.
import { createStore, type Store } from '@molecule/app-state'
import { useStore } from '@molecule/app-react'
const initialState = { sidebarOpen: false }
// Created on first use, NOT at module scope — see the remarks above.
let uiStore: Store<typeof initialState> | undefined
const getUiStore = (): Store<typeof initialState> => (uiStore ??= createStore({ initialState }))
function Sidebar() {
const store = getUiStore()
const { sidebarOpen } = useStore(store) // subscribes to the store
const toggle = () => store.setState({ sidebarOpen: !sidebarOpen })
}Providers (3): @molecule/app-state-jotai, @molecule/app-state-redux, @molecule/app-state-zustand
Works with: @molecule/app-bond, @molecule/app-logger
Reference
Client state management interface for molecule.dev.
Provides a unified state management API that works across different state management solutions (hooks, Zustand, Redux, Jotai, etc.).
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { createStore, type Store } from '@molecule/app-state'
import { useStore } from '@molecule/app-react'
const initialState = { sidebarOpen: false }
// Created on first use, NOT at module scope — see the remarks above.
let uiStore: Store<typeof initialState> | undefined
const getUiStore = (): Store<typeof initialState> => (uiStore ??= createStore({ initialState }))
function Sidebar() {
const store = getUiStore()
const { sidebarOpen } = useStore(store) // subscribes to the store
const toggle = () => store.setState({ sidebarOpen: !sidebarOpen })
}
Type
core
Installation
npm install @molecule/app-state @molecule/app-bond @molecule/app-logger
API
Interfaces
AsyncState
Async state tuple.
interface AsyncState<T> {
/**
* Current data value.
*/
data: T
/**
* Whether the state is loading.
*/
loading: boolean
/**
* Error if any occurred.
*/
error: Error | null
}
AsyncStateActions
Async state actions.
interface AsyncStateActions<T> {
/**
* Sets the data value.
*/
setData(data: T): void
/**
* Sets the loading state.
*/
setLoading(loading: boolean): void
/**
* Sets an error.
*/
setError(error: Error | null): void
/**
* Resets to initial state.
*/
reset(): void
/**
* Executes an async operation, handling loading/error states.
*/
execute<R>(fn: () => Promise<R>): Promise<R>
}
PersistStorage
Simple storage adapter interface for persist middleware.
Compatible with localStorage, sessionStorage, and
@molecule/app-storage providers.
interface PersistStorage {
getItem(key: string): string | null
setItem(key: string, value: string): void
}
StoreConfig
Configuration for creating a store (initial state, optional name, and middleware chain).
interface StoreConfig<T> {
/**
* Initial state value.
*/
initialState: T
/**
* Optional name for debugging.
*/
name?: string
/**
* Optional middleware functions.
*/
middleware?: StoreMiddleware<T>[]
}
Store
Reactive state container with getState, setState, subscribe, and destroy.
All state management providers must implement this interface.
interface Store<T> {
/**
* Gets the current state.
*/
getState(): T
/**
* Sets the state (partial or via updater function).
*/
setState(partial: Partial<T> | ((state: T) => Partial<T>)): void
/**
* Subscribes to state changes.
* Returns an unsubscribe function.
*/
subscribe(listener: StateListener<T>): () => void
/**
* Destroys the store and cleans up subscriptions.
*/
destroy(): void
}
StateProvider
State provider interface that all state management bond packages must implement. Provides the store creation factory.
interface StateProvider {
/**
* Creates a new store.
*/
createStore<T>(config: StoreConfig<T>): Store<T>
}
Types
StoreMiddleware
Store middleware function. Wraps the set function to intercept
state updates (e.g. for logging, persistence, or devtools).
type StoreMiddleware<T> = (set: SetState<T>, get: GetState<T>) => SetState<T>
set— The original setState function to wrap.get— A function returning the current state.
SetState
Function to update store state with a partial object or updater function.
type SetState<T> = (partial: Partial<T> | ((state: T) => Partial<T>)) => void
GetState
Get state function type.
type GetState<T> = () => T
StateListener
Callback invoked whenever store state changes.
type StateListener<T> = (state: T, prevState: T) => void
state— The new state after the update.prevState— The state before the update.
Selector
Selector function that derives a value from store state.
type Selector<T, S> = (state: T) => S
state— The full store state to select from.
EqualityFn
Equality comparator for selectors. When provided, prevents re-renders if the selected value is equal to the previous one.
type EqualityFn<T> = (a: T, b: T) => boolean
Functions
createAsyncState(initialData)
Creates an async state container with loading/error tracking.
function createAsyncState(initialData: T): [AsyncState<T>, AsyncStateActions<T>]
initialData— The initial data value.
Returns: A tuple of [state, actions] for reading and updating the async state.
loggerMiddleware(name)
Logging middleware — logs previous and next state on every update.
function loggerMiddleware(name?: string): StoreMiddleware<T>
name— Optional store name for log prefix (defaults to'store').
Returns: A store middleware that logs state transitions.
persistMiddleware(key, storage)
Persist middleware — saves state to storage on every update and restores it on initialization.
Accepts any object implementing PersistStorage (getItem + setItem).
Defaults to in-memory storage.
function persistMiddleware(key: string, storage?: PersistStorage): StoreMiddleware<T>
key— The storage key to persist state under.storage— APersistStorage-compatible object (defaults to in-memory).
Returns: A store middleware that persists state to the given storage.
setProvider(provider)
Registers a state provider as the active singleton. Called by bond packages during application startup.
function setProvider(provider: StateProvider): void
provider— The state provider implementation to bond.
getProvider()
Retrieves the bonded state provider, throwing if none is configured.
function getProvider(): StateProvider
Returns: The bonded state provider.
hasProvider()
Checks whether a state provider is currently bonded.
function hasProvider(): boolean
Returns: true if a state provider is bonded.
createStore(config)
Creates a new reactive state store using the bonded provider.
function createStore(config: StoreConfig<T>): Store<T>
config— Store configuration including initial state, actions, selectors, and middleware.
Returns: A reactive store instance with getState(), setState(), and subscribe() methods.
createSimpleStateProvider()
Creates a vanilla JavaScript state provider that manages stores with simple object spreading and listener-based subscriptions.
function createSimpleStateProvider(): StateProvider
Returns: A StateProvider implementation.
combineStores(stores)
Combines multiple stores into a single composite store. Each key
in stores becomes a top-level key in the combined state.
function combineStores(stores: { [K in keyof T]: Store<T[K]> }): Store<T>
stores— A record mapping keys to individual stores.
Returns: A composite Store that delegates to the individual stores.
shallowEqual(a, b)
Performs a shallow equality comparison between two values.
Returns true if both values have the same top-level keys
with identical values (using Object.is).
function shallowEqual(a: T, b: T): boolean
a— First value to compare.b— Second value to compare.
Returns: true if the values are shallowly equal.
produce(state, recipe)
Simplified produce helper for immutable state updates. Creates a shallow copy, applies the recipe, and returns the result.
function produce(state: T, recipe: (draft: T) => void): T
state— The current state object.recipe— A function that mutates the draft copy.
Returns: A new state object with the recipe's mutations applied.
Constants
simpleProvider
Pre-created default state provider instance.
const simpleProvider: StateProvider
Available Providers
| Provider | Package |
|---|---|
| Jotai | @molecule/app-state-jotai |
| Redux | @molecule/app-state-redux |
| Zustand | @molecule/app-state-zustand |
Injection Notes
Requirements
Peer dependencies:
@molecule/app-bond^1.0.1@molecule/app-logger^1.0.1
Runtime Dependencies
@molecule/app-bond@molecule/app-logger
Define stores with createStore and read them through the framework hook
(useStore(store) in React / the Vue composable) — do NOT import zustand / redux /
jotai directly in a component; that couples you to one library and breaks the swap. For a
large store, pass a selector via the hook's options so a component re-renders only when the
slice it reads changes.
- This is CLIENT/UI state — NOT the source of truth for server data. Fetch server data
through the HTTP client (
@molecule/app-http) and keep the store for UI/session state. persistMiddlewarepersists to storage — never persist a secret or auth token there (client storage is XSS-exfiltratable; the bearer token is memory-only — see@molecule/app-storage). Persist only non-sensitive UI state, via the storage ABSTRACTION, never rawlocalStorage.- A state provider must be bonded before a store is created. In a scaffolded app that
is automatic:
setupAllDefaultBonds()(from@molecule/app-bonds-default-react, called byapp/src/bonds/index.ts'ssetupProviders()) bonds the vanillasimpleProvider, and the app's<MoleculeProvider>supplies the React contextuseStore()reads. To use another provider, bond it instead (e.g.@molecule/app-state-zustandwithsetProvider()) —createStore()call sites do not change. - Never create a store at module scope. Module bodies run before
setupProviders(), so a top-levelcreateStore()throws@molecule/app-state: No provider setat import time: every page renders blank whiletscpasses. Create it lazily (a getter the component calls) or inside the component, after the provider is set.