Stan.js can be used without React through the stan-js/vanilla package. This is perfect for vanilla JavaScript, non-React frameworks, or backend logic.
Installation
Import from the vanilla subpackage:
import { createStore } from 'stan-js/vanilla'
Basic Usage
Create a store and interact with it imperatively:
import { createStore } from 'stan-js/vanilla'
const store = createStore ({
counter: 0 ,
message: 'Hello' ,
users: [] as Array < string >
})
// Read state
console . log ( store . getState (). counter ) // 0
// Update state
store . actions . setCounter ( 5 )
store . actions . setMessage ( 'World' )
// Functional updates
store . actions . setCounter ( prev => prev + 1 )
console . log ( store . getState (). counter ) // 6
Core API
getState()
Retrieve the current state:
const state = store . getState ()
console . log ( state . counter , state . message )
actions
Auto-generated setter functions for each state property:
// Direct value
store . actions . setCounter ( 10 )
// Functional update
store . actions . setCounter ( prev => prev * 2 )
// Arrays and objects
store . actions . setUsers ( prev => [ ... prev , 'Alice' ])
subscribe()
Listen to state changes:
const unsubscribe = store . subscribe ([ 'counter' ])( newState => {
console . log ( 'Counter changed:' , store . getState (). counter )
})
// Stop listening
unsubscribe ()
The subscribe function takes an array of keys to watch. Only changes to those keys will trigger the callback.
effect()
A more convenient way to subscribe that auto-tracks dependencies:
const dispose = store . effect ( state => {
console . log ( 'Counter or message changed:' , state . counter , state . message )
})
// Only runs when counter or message changes
store . actions . setCounter ( 5 ) // Triggers
store . actions . setUsers ([ 'Bob' ]) // Doesn't trigger
// Stop listening
dispose ()
The effect automatically tracks which properties you access:
store . effect (({ counter }) => {
console . log ( 'Counter:' , counter )
// Only subscribes to 'counter', not other properties
})
reset()
Reset state to initial values:
// Reset specific keys
store . reset ( 'counter' , 'message' )
// Reset everything
store . reset ()
batchUpdates()
Batch multiple updates to trigger listeners only once:
store . batchUpdates (() => {
store . actions . setCounter ( 0 )
store . actions . setMessage ( 'Reset' )
store . actions . setUsers ([])
})
// Listeners fire only once after all updates
Computed Values
Use getters for derived state:
const store = createStore ({
firstName: 'John' ,
lastName: 'Doe' ,
get fullName () {
return ` ${ this . firstName } ${ this . lastName } `
}
})
console . log ( store . getState (). fullName ) // 'John Doe'
store . actions . setFirstName ( 'Jane' )
console . log ( store . getState (). fullName ) // 'Jane Doe'
Computed values automatically update when dependencies change:
store . effect (({ fullName }) => {
console . log ( 'Full name:' , fullName )
})
store . actions . setFirstName ( 'Bob' ) // Triggers effect
store . actions . setLastName ( 'Smith' ) // Triggers effect
Custom Actions
Create reusable actions with complex logic:
const store = createStore (
{
firstName: 'John' ,
lastName: 'Doe' ,
email: ''
},
({ actions , getState , reset }) => ({
setUser : ( firstName : string , lastName : string , email : string ) => {
actions . setFirstName ( firstName )
actions . setLastName ( lastName )
actions . setEmail ( email )
},
clearUser : () => {
reset ()
},
updateEmail : ( email : string ) => {
if ( email . includes ( '@' )) {
actions . setEmail ( email )
}
}
})
)
// Use custom actions
store . actions . setUser ( 'Jane' , 'Smith' , '[email protected] ' )
store . actions . clearUser ()
store . actions . updateEmail ( 'invalid' ) // No update
store . actions . updateEmail ( '[email protected] ' ) // Updates
Custom actions are automatically batched, so multiple updates inside a custom action only trigger listeners once.
Integration Examples
Vanilla JavaScript
<! DOCTYPE html >
< html >
< head >
< title > Stan.js Vanilla Demo </ title >
</ head >
< body >
< div id = "counter" > 0 </ div >
< button id = "increment" > + </ button >
< button id = "decrement" > - </ button >
< script type = "module" >
import { createStore } from 'stan-js/vanilla'
const store = createStore ({ counter: 0 })
// Update DOM
store . effect (({ counter }) => {
document . getElementById ( 'counter' ). textContent = counter
})
// Handle clicks
document . getElementById ( 'increment' ). onclick = () => {
store . actions . setCounter ( prev => prev + 1 )
}
document . getElementById ( 'decrement' ). onclick = () => {
store . actions . setCounter ( prev => prev - 1 )
}
</ script >
</ body >
</ html >
Vue.js
import { createStore } from 'stan-js/vanilla'
import { reactive , watchEffect } from 'vue'
const stanStore = createStore ({
counter: 0 ,
message: 'Hello'
})
export const useStore = () => {
const state = reactive ( stanStore . getState ())
// Sync Stan.js changes to Vue reactive state
stanStore . effect ( newState => {
Object . assign ( state , newState )
})
return {
state ,
actions: stanStore . actions
}
}
Svelte
import { createStore } from 'stan-js/vanilla'
import { writable } from 'svelte/store'
const stanStore = createStore ({
counter: 0 ,
message: 'Hello'
})
export const counter = writable ( stanStore . getState (). counter )
stanStore . effect (({ counter : newCounter }) => {
counter . set ( newCounter )
})
export const setCounter = stanStore . actions . setCounter
Node.js / Backend
import { createStore } from 'stan-js/vanilla'
const sessionStore = createStore ({
activeSessions: new Map < string , Session >(),
requestCount: 0
})
// Track requests
app . use (( req , res , next ) => {
sessionStore . actions . setRequestCount ( prev => prev + 1 )
next ()
})
// Monitor state
sessionStore . effect (({ requestCount }) => {
if ( requestCount > 10000 ) {
console . warn ( 'High request volume detected' )
}
})
Stan.js exposes stores globally for debugging:
// In browser console
globalThis [ '__stan-js__' ]
// Array of all stores with their state and methods
const { store , updateStore } = globalThis [ '__stan-js__' ][ 0 ]
console . log ( store ) // Current state
updateStore ({ counter: 100 }) // Update from DevTools
Selective Subscriptions Only subscribe to the keys you need. This minimizes unnecessary listener calls.
Batch Updates Use batchUpdates() when making multiple changes to trigger listeners only once.
Computed Values Computed values are memoized and only recalculated when dependencies change.
Dispose Listeners Always dispose of effects and subscriptions when they’re no longer needed to prevent memory leaks.
TypeScript Support
Full type inference and type safety:
const store = createStore ({
counter: 0 ,
message: 'hello'
})
const state = store . getState ()
// ^? { counter: number, message: string }
store . actions . setCounter ( 5 ) // ✓
store . actions . setCounter ( 'invalid' ) // ✗ Type error
store . effect (({ counter }) => {
console . log ( counter . toFixed ( 2 )) // ✓ TypeScript knows counter is a number
})
See the TypeScript guide for advanced patterns.