DeployStack Docs

Frontend Storage System

The storage system is built into the global event bus and provides persistent data management across route changes and browser sessions. This system uses localStorage with a type-safe API and automatically emits events when data changes.

📖 For event bus fundamentals, see Global Event Bus

Overview

The storage system solves common frontend challenges such as:

  • Persistent State: Maintain application state across route changes and page refreshes
  • Type Safety: Full TypeScript support with generic methods
  • Easy Integration: Simple API that works with the existing event bus
  • Automatic Cleanup: Consistent storage key management with prefixing
  • Event Integration: Storage changes emit events for reactive updates

Architecture

Storage Configuration

The storage system is built into the event bus and uses a centralized configuration:

// Storage configuration in useEventBus.ts
const STORAGE_CONFIG = {
  prefix: 'deploystack_',
  keys: {
    SELECTED_TEAM_ID: 'selected_team_id',
    // Add new keys here as needed
  }
}

Type Safety

All storage operations are type-safe using TypeScript generics:

// Generic storage methods
setState<T>(key: string, value: T): void
getState<T>(key: string, defaultValue?: T): T | null
clearState(key: string): void
hasState(key: string): boolean

Usage

Basic Storage Operations

Storing Data

import { useEventBus } from '@/composables/useEventBus'

const eventBus = useEventBus()

// Store a string
eventBus.setState('selected_team_id', 'team-123')

// Store an object
eventBus.setState('user_preferences', {
  theme: 'dark',
  language: 'en',
  notifications: true
})

// Store an array
eventBus.setState('recent_searches', ['query1', 'query2', 'query3'])

// Store a boolean
eventBus.setState('sidebar_collapsed', true)

Retrieving Data

// Get data with type safety
const teamId = eventBus.getState<string>('selected_team_id')

// Get data with default value
const theme = eventBus.getState<string>('selected_theme', 'light')

// Get complex objects
interface UserPreferences {
  theme: string
  language: string
  notifications: boolean
}

const preferences = eventBus.getState<UserPreferences>('user_preferences')

Checking and Clearing Data

// Check if data exists
if (eventBus.hasState('selected_team_id')) {
  console.log('Team selection exists')
}

// Clear specific data
eventBus.clearState('selected_team_id')

// Get all stored data
const allData = eventBus.getAllState()
console.log('All stored data:', allData)

// Clear all stored data
eventBus.clearAllState()

Storage Key Naming Convention

The storage system follows strict naming conventions to ensure consistency and prevent conflicts across the application.

Naming Pattern

{feature}_{description}

All storage keys should:

  • Use snake_case (lowercase with underscores)
  • Start with the feature name or domain
  • End with a descriptive identifier
  • Be specific and meaningful

Examples

// Good storage key names
'selected_team_id'          // Team selection
'user_preferences'          // User preferences object
'dashboard_layout'          // Dashboard configuration
'recent_searches'           // Search history
'sidebar_collapsed'         // UI state
'notification_settings'     // Notification preferences
'selected_theme'            // Theme selection
'last_visited_page'         // Navigation tracking

// Avoid these patterns
'data'                      // Too generic
'temp'                      // Not descriptive
'userPref'                  // Use snake_case, not camelCase
'TEAM_ID'                   // Use lowercase, not uppercase
'team-id'                   // Use underscores, not hyphens

Storage Key Categories

  1. UI State: {component}_{state}

    • sidebar_collapsed
    • modal_shown
    • tab_selected
  2. User Preferences: {feature}_preferences or user_{setting}

    • notification_preferences
    • user_language
    • user_timezone
  3. Selection State: selected_{entity}_id

    • selected_team_id
    • selected_project_id
    • selected_workspace_id
  4. Cache/History: {feature}_history or recent_{items}

    • search_history
    • recent_searches
    • visited_pages
  5. Feature Data: {feature}_{data_type}

    • dashboard_layout
    • form_draft
    • wizard_state

Adding New Storage Values

Step 1: Define Your Storage Key

Follow the naming convention and optionally add your key to the configuration for better organization:

// In /composables/useEventBus.ts
const STORAGE_CONFIG = {
  prefix: 'deploystack_',
  keys: {
    SELECTED_TEAM_ID: 'selected_team_id',
    SELECTED_THEME: 'selected_theme',           // NEW - follows pattern
    USER_DASHBOARD_LAYOUT: 'dashboard_layout',  // NEW - follows pattern
    RECENT_SEARCHES: 'recent_searches',         // NEW - follows pattern
  }
}

Step 2: Use in Components

// In any Vue component
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useEventBus } from '@/composables/useEventBus'

const eventBus = useEventBus()
const selectedTheme = ref<string>('light')

// Initialize from storage
onMounted(() => {
  const storedTheme = eventBus.getState<string>('selected_theme', 'light')
  selectedTheme.value = storedTheme
})

// Update theme and store it
const changeTheme = (newTheme: string) => {
  selectedTheme.value = newTheme
  eventBus.setState('selected_theme', newTheme)
}
</script>

Step 3: Listen for Storage Changes (Optional)

// Listen for storage change events
eventBus.on('storage-changed', (data) => {
  console.log(`Storage changed: ${data.key}`, {
    oldValue: data.oldValue,
    newValue: data.newValue
  })
})

Real-World Examples

Example 1: Theme Persistence

// ThemeManager.vue
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { useEventBus } from '@/composables/useEventBus'

const eventBus = useEventBus()
const currentTheme = ref<'light' | 'dark'>('light')

// Initialize theme from storage
onMounted(() => {
  const storedTheme = eventBus.getState<'light' | 'dark'>('selected_theme', 'light')
  currentTheme.value = storedTheme
  applyTheme(storedTheme)
})

// Watch for theme changes and persist them
watch(currentTheme, (newTheme) => {
  eventBus.setState('selected_theme', newTheme)
  applyTheme(newTheme)
})

const toggleTheme = () => {
  currentTheme.value = currentTheme.value === 'light' ? 'dark' : 'light'
}

const applyTheme = (theme: string) => {
  document.documentElement.setAttribute('data-theme', theme)
}
</script>

Example 2: Dashboard Layout Persistence

// DashboardLayout.vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useEventBus } from '@/composables/useEventBus'

interface DashboardLayout {
  sidebarWidth: number
  panelOrder: string[]
  hiddenPanels: string[]
}

const eventBus = useEventBus()
const layout = ref<DashboardLayout>({
  sidebarWidth: 250,
  panelOrder: ['metrics', 'logs', 'alerts'],
  hiddenPanels: []
})

// Initialize layout from storage
onMounted(() => {
  const storedLayout = eventBus.getState<DashboardLayout>('dashboard_layout')
  if (storedLayout) {
    layout.value = { ...layout.value, ...storedLayout }
  }
})

// Save layout changes
const updateLayout = (changes: Partial<DashboardLayout>) => {
  layout.value = { ...layout.value, ...changes }
  eventBus.setState('dashboard_layout', layout.value)
}

// Example usage
const resizeSidebar = (width: number) => {
  updateLayout({ sidebarWidth: width })
}

const reorderPanels = (newOrder: string[]) => {
  updateLayout({ panelOrder: newOrder })
}
</script>

Best Practices

1. Follow Naming Conventions

Always use the established naming pattern {feature}_{description} with snake_case:

// ✅ Good - follows convention
eventBus.setState('selected_team_id', teamId)
eventBus.setState('user_dashboard_layout', layout)
eventBus.setState('notification_preferences', prefs)
eventBus.setState('sidebar_collapsed', true)

// ❌ Bad - violates convention
eventBus.setState('data', someData)           // Too generic
eventBus.setState('selectedTeamId', teamId)   // Wrong case (camelCase)
eventBus.setState('TEAM_ID', teamId)          // Wrong case (uppercase)
eventBus.setState('team-id', teamId)          // Wrong separator (hyphen)

2. Provide Default Values

// Good - provides fallback
const theme = eventBus.getState<string>('selected_theme', 'light')
const layout = eventBus.getState<LayoutConfig>('dashboard_layout', defaultLayout)

// Less robust - might return null
const theme = eventBus.getState<string>('selected_theme')

3. Use Type Safety

// Good - type-safe
interface UserPreferences {
  theme: 'light' | 'dark'
  language: string
  notifications: boolean
}

const prefs = eventBus.getState<UserPreferences>('user_preferences')

// Less safe - no type checking
const prefs = eventBus.getState('user_preferences')

4. Handle Storage Errors Gracefully

// The storage system handles errors internally, but you can add extra validation
const getStoredTeamId = (): string | null => {
  try {
    const teamId = eventBus.getState<string>('selected_team_id')
    
    // Additional validation
    if (teamId && teamId.length > 0) {
      return teamId
    }
    
    return null
  } catch (error) {
    console.warn('Failed to get stored team ID:', error)
    return null
  }
}

5. Clean Up When Appropriate

// Clear storage when user logs out
const logout = () => {
  // Clear user-specific data
  eventBus.clearState('selected_team_id')
  eventBus.clearState('user_preferences')
  eventBus.clearState('dashboard_layout')
  
  // Or clear everything
  eventBus.clearAllState()
  
  // Proceed with logout...
}

Storage Events

The storage system emits events when data changes, allowing for reactive updates:

// Listen for any storage changes
eventBus.on('storage-changed', (data) => {
  console.log(`Storage key "${data.key}" changed:`, {
    from: data.oldValue,
    to: data.newValue
  })
})

// React to specific storage changes
eventBus.on('storage-changed', (data) => {
  if (data.key === 'selected_theme') {
    applyTheme(data.newValue)
  }
})

Technical Details

Storage Implementation

  • Prefix: All keys are prefixed with deploystack_ to avoid conflicts
  • Serialization: Data is stored as JSON strings using safe parsing
  • Error Handling: All storage operations include try-catch blocks
  • Type Safety: Generic methods provide compile-time type checking
  • Event Integration: Storage changes emit storage-changed events

Browser Compatibility

The storage system uses localStorage, which is supported in all modern browsers. The system gracefully handles storage errors (e.g., when localStorage is disabled or full).

Performance Considerations

  • Synchronous Operations: localStorage operations are synchronous but fast
  • JSON Serialization: Large objects may impact performance during serialization
  • Storage Limits: localStorage typically has a 5-10MB limit per domain
  • Event Frequency: Storage change events are emitted for every setState/clearState call

The enhanced event bus storage system provides a powerful, type-safe way to manage persistent state in the DeployStack frontend, making it easy to maintain user preferences and application state across sessions.