State Management
Working with PACE.js's reactive state system.
Overview
PACE.js uses a lightweight, reactive state manager based on the Observer pattern.
javascript
import { State } from '@semanticintent/pace-pattern'
const state = new State({
activeView: 'product',
selectedProduct: null
})State Structure
Default state shape:
javascript
{
// View Management
activeView: 'product' | 'about' | 'chat' | 'summary',
// Product State
selectedProduct: Product | null,
filteredProducts: Product[],
searchQuery: string | null,
activeCategory: string | null,
// Chat State
chatHistory: Message[],
isTyping: boolean,
// Executive Summary State
executiveSummaryData: {
conversationSummary: string,
productsDiscussed: Product[],
userExpertise: 'beginner' | 'intermediate' | 'advanced',
suggestedNextSteps: string[]
},
// UI State
sidebarOpen: boolean,
modalOpen: boolean
}Reading State
Get Single Value
javascript
const view = state.get('activeView')
console.log(view) // 'product'Get All State
javascript
const allState = state.getAll()
console.log(allState)Updating State
Set Single Value
javascript
state.set('activeView', 'chat')Set Multiple Values
javascript
state.set('selectedProduct', product)
state.set('activeView', 'product')Update Nested State
javascript
const summary = state.get('executiveSummaryData')
state.set('executiveSummaryData', {
...summary,
userExpertise: 'advanced'
})Subscribing to Changes
Basic Subscription
javascript
state.subscribe('activeView', (newValue, oldValue) => {
console.log(`View changed: ${oldValue} → ${newValue}`)
})
// Trigger
state.set('activeView', 'chat')
// Logs: "View changed: product → chat"Unsubscribe
javascript
const unsubscribe = state.subscribe('activeView', callback)
// Later...
unsubscribe()Multiple Subscriptions
javascript
// Subscription 1
state.subscribe('selectedProduct', (product) => {
updateProductDetails(product)
})
// Subscription 2
state.subscribe('selectedProduct', (product) => {
trackAnalytics('product_view', { id: product.id })
})
// Both fire when state changes
state.set('selectedProduct', newProduct)Computed Values
Create derived state:
javascript
class EnhancedState extends State {
get productCount() {
return this.get('filteredProducts').length
}
get hasSelection() {
return this.get('selectedProduct') !== null
}
get chatMessageCount() {
return this.get('chatHistory').length
}
}State Validation
Validate before setting:
javascript
class ValidatedState extends State {
set(key, value) {
// Validate
if (key === 'activeView') {
const validViews = ['product', 'about', 'chat', 'summary']
if (!validViews.includes(value)) {
throw new Error(`Invalid view: ${value}`)
}
}
// Call parent
super.set(key, value)
}
}State Persistence
LocalStorage
javascript
// Save state
state.subscribe('*', () => {
localStorage.setItem('paceState', JSON.stringify(state.getAll()))
})
// Load state
const savedState = localStorage.getItem('paceState')
if (savedState) {
const state = new State(JSON.parse(savedState))
}SessionStorage
javascript
// Persist for session only
state.subscribe('*', () => {
sessionStorage.setItem('paceState', JSON.stringify(state.getAll()))
})State Middleware
Add logging, analytics, or validation:
javascript
class MiddlewareState extends State {
set(key, value) {
// Log
console.log(`[State] ${key}:`, value)
// Analytics
gtag('event', 'state_change', { key, value })
// Validate
this.validate(key, value)
// Call parent
super.set(key, value)
}
validate(key, value) {
// Custom validation
}
}React Integration
With Context
jsx
import { createContext, useContext, useState, useEffect } from 'react'
import { State } from '@semanticintent/pace-pattern'
const StateContext = createContext(null)
export function StateProvider({ children }) {
const [state] = useState(() => new State())
return (
<StateContext.Provider value={state}>
{children}
</StateContext.Provider>
)
}
export function useStateValue(key) {
const state = useContext(StateContext)
const [value, setValue] = useState(state.get(key))
useEffect(() => {
return state.subscribe(key, setValue)
}, [state, key])
return [value, (newValue) => state.set(key, newValue)]
}
// Usage
function ProductView() {
const [selectedProduct, setSelectedProduct] = useStateValue('selectedProduct')
return (
<div>
{selectedProduct ? selectedProduct.name : 'No selection'}
</div>
)
}Vue Integration
vue
<template>
<div>{{ activeView }}</div>
</template>
<script>
import { reactive, computed } from 'vue'
import { State } from '@semanticintent/pace-pattern'
export default {
setup() {
const state = new State()
const reactiveState = reactive({
activeView: computed(() => state.get('activeView'))
})
state.subscribe('activeView', (value) => {
reactiveState.activeView = value
})
return { reactiveState }
}
}
</script>Debugging
State Logger Plugin
javascript
const stateLoggerPlugin = {
name: 'state-logger',
install(pace) {
pace.state.subscribe('*', (key, value, oldValue) => {
console.group(`[State] ${key}`)
console.log('Old:', oldValue)
console.log('New:', value)
console.groupEnd()
})
}
}
pace.use(stateLoggerPlugin)DevTools Integration
javascript
if (window.__REDUX_DEVTOOLS_EXTENSION__) {
const devtools = window.__REDUX_DEVTOOLS_EXTENSION__.connect()
state.subscribe('*', () => {
devtools.send('STATE_UPDATE', state.getAll())
})
}Best Practices
1. Keep State Flat
✅ Good:
javascript
{
selectedProductId: 'sql-mcp',
products: [...]
}❌ Bad:
javascript
{
products: {
items: [...],
selected: {
id: 'sql-mcp',
details: {...}
}
}
}2. Use Immutable Updates
✅ Good:
javascript
const history = state.get('chatHistory')
state.set('chatHistory', [...history, newMessage])❌ Bad:
javascript
const history = state.get('chatHistory')
history.push(newMessage)
state.set('chatHistory', history)3. Batch Updates
javascript
// Instead of multiple sets
state.set('loading', true)
state.set('error', null)
state.set('data', null)
// Use a single update
state.set('loadingState', {
loading: true,
error: null,
data: null
})Master PACE state management! 🧠