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
-
UI State:
{component}_{state}
sidebar_collapsed
modal_shown
tab_selected
-
User Preferences:
{feature}_preferences
oruser_{setting}
notification_preferences
user_language
user_timezone
-
Selection State:
selected_{entity}_id
selected_team_id
selected_project_id
selected_workspace_id
-
Cache/History:
{feature}_history
orrecent_{items}
search_history
recent_searches
visited_pages
-
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
Related Documentation
- Global Event Bus - Core event system that powers storage
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.