Skip to content

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! 🧠