Overview
The effect method creates a subscription that automatically tracks which state properties you access and re-runs when those properties change. It’s the recommended way to react to state changes in vanilla JavaScript.
Signature
function effect(run: (state: TState) => void): () => void
Parameters
run
(state: TState) => void
required
Callback function that runs immediately and whenever tracked dependencies change.
- Receives the current state as a parameter
- Automatically tracks which properties you access
- Re-runs only when accessed properties change
- Can return cleanup logic (not captured)
Return Value
Function to stop the effect and clean up the subscription.Call this when you’re done listening to state changes to prevent memory leaks.
How Dependency Tracking Works
The effect uses a Proxy to track which state properties you access during the first run:
const store = createStore({
count: 0,
name: 'Alice',
theme: 'light'
})
// This effect only tracks 'count' because that's the only property accessed
store.effect(state => {
console.log('Count is:', state.count)
})
// This triggers the effect
store.actions.setCount(1) // Logs: "Count is: 1"
// These don't trigger the effect (different properties)
store.actions.setName('Bob') // No log
store.actions.setTheme('dark') // No log
Basic Example
import { createStore } from '@codemask-labs/stan-js/vanilla'
const store = createStore({
count: 0
})
const unsubscribe = store.effect(state => {
console.log('Count changed to:', state.count)
})
// Immediately logs: "Count changed to: 0"
store.actions.setCount(1)
// Logs: "Count changed to: 1"
store.actions.setCount(2)
// Logs: "Count changed to: 2"
// Clean up when done
unsubscribe()
store.actions.setCount(3)
// No log (unsubscribed)
Multiple Dependencies
Access multiple properties to track all of them:
const store = createStore({
firstName: 'John',
lastName: 'Doe',
age: 30
})
store.effect(state => {
console.log(`${state.firstName} ${state.lastName} is ${state.age} years old`)
})
// Logs: "John Doe is 30 years old"
store.actions.setFirstName('Jane')
// Logs: "Jane Doe is 30 years old"
store.actions.setAge(31)
// Logs: "Jane Doe is 31 years old"
Conditional Dependencies
Only properties accessed during the first run are tracked:
const store = createStore({
showAdvanced: false,
basicSetting: 'A',
advancedSetting: 'X'
})
store.effect(state => {
console.log('Basic:', state.basicSetting)
if (state.showAdvanced) {
console.log('Advanced:', state.advancedSetting)
}
})
// Initially: showAdvanced is false, so advancedSetting is NOT tracked
// This triggers the effect
store.actions.setBasicSetting('B')
// This does NOT trigger the effect (not accessed in first run)
store.actions.setAdvancedSetting('Y')
// Note: If you toggle showAdvanced, you need a new effect to track it
DOM Updates
Use effects to update the DOM:
const store = createStore({
count: 0,
status: 'idle'
})
// Update counter display
store.effect(state => {
const element = document.getElementById('counter')
if (element) {
element.textContent = state.count
}
})
// Update status indicator
store.effect(state => {
const element = document.getElementById('status')
if (element) {
element.className = `status-${state.status}`
element.textContent = state.status
}
})
Side Effects
Trigger side effects when state changes:
const store = createStore({
searchQuery: '',
filters: [],
sortBy: 'date'
})
store.effect(state => {
// Trigger search when query or filters change
if (state.searchQuery) {
fetch(`/api/search?q=${state.searchQuery}&filters=${state.filters.join(',')}`)
.then(res => res.json())
.then(results => {
store.actions.setResults(results)
})
}
})
Local Storage Sync
const store = createStore({
theme: 'light',
language: 'en',
notifications: true
})
// Persist settings to localStorage
store.effect(state => {
const settings = {
theme: state.theme,
language: state.language,
notifications: state.notifications
}
localStorage.setItem('settings', JSON.stringify(settings))
})
Computed Logging
Track computed values:
const store = createStore({
items: [],
get itemCount() {
return this.items.length
},
get hasItems() {
return this.items.length > 0
}
})
store.effect(state => {
console.log('Item count:', state.itemCount)
console.log('Has items:', state.hasItems)
})
store.actions.setItems([1, 2, 3])
// Logs:
// "Item count: 3"
// "Has items: true"
Cleanup Pattern
Manage subscriptions with cleanup:
const store = createStore({
isActive: false
})
let intervalId
const unsubscribe = store.effect(state => {
if (state.isActive) {
// Start interval when active
intervalId = setInterval(() => {
console.log('Tick...')
}, 1000)
} else {
// Clear interval when inactive
if (intervalId) {
clearInterval(intervalId)
intervalId = null
}
}
})
// Don't forget to clean up the effect itself
function cleanup() {
unsubscribe()
if (intervalId) {
clearInterval(intervalId)
}
}
No Dependencies (Listen to All)
If you don’t access any properties, the effect listens to all changes:
const store = createStore({
a: 1,
b: 2,
c: 3
})
store.effect(() => {
// Not accessing state.anything
console.log('Store updated!')
})
// All of these trigger the effect
store.actions.setA(10)
store.actions.setB(20)
store.actions.setC(30)
Multiple Effects
Create multiple independent effects:
const store = createStore({
count: 0,
message: ''
})
const unsubscribe1 = store.effect(state => {
console.log('Count effect:', state.count)
})
const unsubscribe2 = store.effect(state => {
console.log('Message effect:', state.message)
})
store.actions.setCount(5)
// Only logs: "Count effect: 5"
store.actions.setMessage('Hello')
// Only logs: "Message effect: Hello"
// Clean up both
unsubscribe1()
unsubscribe2()
Type Safety
TypeScript provides full type inference:
interface State {
user: { id: number; name: string } | null
isLoading: boolean
}
const store = createStore<State>({
user: null,
isLoading: false
})
store.effect(state => {
// TypeScript knows the types
const userId: number | undefined = state.user?.id
const loading: boolean = state.isLoading
if (state.user) {
console.log('Logged in as:', state.user.name)
}
})
Effects only run when tracked properties change. Be mindful of what you access:
const store = createStore({
bigArray: Array(1000).fill(0).map((_, i) => ({ id: i })),
selectedId: 0
})
// ❌ Inefficient - runs on any bigArray change
store.effect(state => {
const selected = state.bigArray.find(item => item.id === state.selectedId)
console.log('Selected:', selected)
})
// ✓ Better - only runs when selectedId changes
store.effect(state => {
console.log('Selected ID:', state.selectedId)
})
See Also