Best Practices
This guide covers best practices for using EwVueComponent effectively in your Vue 3 applications.
Performance Optimization
1. Efficient Component Caching
Use caching strategically for components that are expensive to create or load:
<template>
<EwVueComponent
:is="currentComponent"
:cache="shouldCache"
:cache-key="cacheKey"
:cache-ttl="cacheTtl"
/>
</template>
<script setup>
import { ref, computed } from 'vue'
import { EwVueComponent } from 'ew-vue-component'
const currentComponent = ref(null)
const componentType = ref('light') // light, medium, heavy
// Cache expensive components, skip caching for lightweight ones
const shouldCache = computed(() => componentType.value !== 'light')
// Use component size and unique identifier as cache key
const cacheKey = computed(() => `${componentType.value}-${componentId.value}`)
// Adjust TTL based on component characteristics
const cacheTtl = computed(() => {
switch (componentType.value) {
case 'heavy': return 600000 // 10 minutes for heavy components
case 'medium': return 300000 // 5 minutes for medium components
default: return 60000 // 1 minute for light components
}
})
</script>
2. Lazy Loading Strategy
Implement progressive loading for better initial page performance:
<template>
<div class="app">
<!-- Critical components load immediately -->
<EwVueComponent :is="HeaderComponent" />
<!-- Non-critical components load on demand -->
<EwVueComponent
v-if="showSidebar"
:is="SidebarComponent"
:cache="true"
/>
<!-- Heavy components load when needed -->
<EwVueComponent
v-if="showDashboard"
:is="DashboardComponent"
:cache="true"
:plugins="[performancePlugin]"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { EwVueComponent } from 'ew-vue-component'
// Immediate components
import HeaderComponent from '@/components/HeaderComponent.vue'
// Lazy-loaded components
const SidebarComponent = () => import('@/components/SidebarComponent.vue')
const DashboardComponent = () => import('@/components/DashboardComponent.vue')
const showSidebar = ref(false)
const showDashboard = ref(false)
const performancePlugin = {
name: 'performance',
beforeRender(component, props, context) {
console.time(`render-${component.name}`)
},
afterRender(component, props, context) {
console.timeEnd(`render-${component.name}`)
}
}
</script>
3. Component Preloading
Preload components that users are likely to interact with:
// utils/componentPreloader.js
export class ComponentPreloader {
constructor() {
this.preloadQueue = new Set()
this.loadedComponents = new Map()
}
async preload(componentLoaders, priority = 'low') {
const promises = Object.entries(componentLoaders).map(async ([name, loader]) => {
if (this.loadedComponents.has(name)) {
return this.loadedComponents.get(name)
}
try {
const component = await loader()
this.loadedComponents.set(name, component)
return component
} catch (error) {
console.warn(`Failed to preload component ${name}:`, error)
return null
}
})
if (priority === 'high') {
return Promise.all(promises)
} else {
// Low priority: load during idle time
return new Promise(resolve => {
requestIdleCallback(() => {
Promise.all(promises).then(resolve)
})
})
}
}
get(name) {
return this.loadedComponents.get(name)
}
}
// Usage
const preloader = new ComponentPreloader()
// Preload critical components
preloader.preload({
UserProfile: () => import('@/components/UserProfile.vue'),
Settings: () => import('@/components/Settings.vue')
}, 'high')
// Preload non-critical components during idle time
preloader.preload({
Dashboard: () => import('@/components/Dashboard.vue'),
Reports: () => import('@/components/Reports.vue')
}, 'low')
Error Handling Strategies
1. Graceful Degradation
Implement fallback mechanisms for robust applications:
<template>
<EwVueComponent
:is="currentComponent"
:fallback="FallbackComponent"
:error-component="ErrorComponent"
@error="handleComponentError"
>
<!-- Always provide meaningful default content -->
<template #default>
<div class="default-content">
<h3>Loading...</h3>
<p>Please wait while we load the content.</p>
</div>
</template>
</EwVueComponent>
</template>
<script setup>
import { ref, h } from 'vue'
import { EwVueComponent } from 'ew-vue-component'
const currentComponent = ref(null)
// Simple fallback component
const FallbackComponent = {
render() {
return h('div', { class: 'fallback-component' }, [
h('h3', 'Service Temporarily Unavailable'),
h('p', 'We\'re working to restore this feature. Please try again later.'),
h('button', {
onClick: () => window.location.reload(),
class: 'retry-button'
}, 'Refresh Page')
])
}
}
// Detailed error component
const ErrorComponent = {
props: ['error', 'retry'],
render() {
const isDevelopment = process.env.NODE_ENV === 'development'
return h('div', { class: 'error-boundary' }, [
h('h3', 'Something went wrong'),
h('p', 'We apologize for the inconvenience. The component failed to load.'),
isDevelopment && h('details', [
h('summary', 'Error Details (Development)'),
h('pre', this.error?.stack || this.error?.message)
]),
h('div', { class: 'error-actions' }, [
h('button', { onClick: this.retry }, 'Try Again'),
h('button', {
onClick: () => window.history.back(),
class: 'secondary'
}, 'Go Back')
])
])
}
}
const handleComponentError = (error) => {
// Log error for monitoring
console.error('Component error:', error)
// Report to error tracking service
if (window.Sentry) {
window.Sentry.captureException(error)
}
// Show user notification
showNotification({
type: 'error',
message: 'Something went wrong. Please try refreshing the page.',
duration: 5000
})
}
</script>
<style scoped>
.fallback-component {
padding: 2rem;
text-align: center;
background: #fef3c7;
border: 1px solid #f59e0b;
border-radius: 0.5rem;
}
.error-boundary {
padding: 2rem;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 0.5rem;
color: #dc2626;
}
.error-actions {
margin-top: 1rem;
display: flex;
gap: 0.5rem;
justify-content: center;
}
.retry-button,
.error-actions button {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
cursor: pointer;
}
.retry-button {
background: #f59e0b;
color: white;
}
.error-actions button {
background: #dc2626;
color: white;
}
.error-actions button.secondary {
background: #6b7280;
}
</style>
2. Error Monitoring and Analytics
Track component errors for continuous improvement:
// utils/errorTracker.js
class ComponentErrorTracker {
constructor(options = {}) {
this.endpoint = options.endpoint || '/api/errors'
this.enabled = options.enabled !== false
this.maxRetries = options.maxRetries || 3
this.retryDelay = options.retryDelay || 1000
}
async track(error, context) {
if (!this.enabled) return
const errorData = {
message: error.message,
stack: error.stack,
component: context.component?.name || 'Unknown',
timestamp: new Date().toISOString(),
url: window.location.href,
userAgent: navigator.userAgent,
userId: this.getCurrentUserId(),
sessionId: this.getSessionId(),
buildVersion: process.env.VUE_APP_VERSION
}
await this.sendWithRetry(errorData)
}
async sendWithRetry(data, attempt = 1) {
try {
await fetch(this.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
} catch (error) {
if (attempt < this.maxRetries) {
setTimeout(() => {
this.sendWithRetry(data, attempt + 1)
}, this.retryDelay * attempt)
} else {
console.warn('Failed to send error report after', this.maxRetries, 'attempts')
}
}
}
getCurrentUserId() {
// Implement based on your auth system
return localStorage.getItem('userId') || 'anonymous'
}
getSessionId() {
// Implement session tracking
return sessionStorage.getItem('sessionId') || 'unknown'
}
}
// Usage
const errorTracker = new ComponentErrorTracker({
endpoint: '/api/component-errors',
enabled: process.env.NODE_ENV === 'production'
})
export { errorTracker }
Code Organization
1. Component Mapping Strategy
Organize components systematically:
// config/componentMap.js
export const componentMap = {
// Core UI components
ui: {
Button: () => import('@/components/ui/Button.vue'),
Modal: () => import('@/components/ui/Modal.vue'),
Table: () => import('@/components/ui/Table.vue')
},
// Feature-specific components
user: {
Profile: () => import('@/components/user/Profile.vue'),
Settings: () => import('@/components/user/Settings.vue'),
Avatar: () => import('@/components/user/Avatar.vue')
},
// Page components
pages: {
Dashboard: () => import('@/pages/Dashboard.vue'),
Analytics: () => import('@/pages/Analytics.vue'),
Reports: () => import('@/pages/Reports.vue')
},
// Admin components (role-based)
admin: {
UserManagement: () => import('@/components/admin/UserManagement.vue'),
SystemSettings: () => import('@/components/admin/SystemSettings.vue')
}
}
// Component resolver utility
export class ComponentResolver {
constructor(componentMap) {
this.componentMap = componentMap
}
resolve(path) {
const [category, component] = path.split('.')
if (!this.componentMap[category]) {
throw new Error(`Component category "${category}" not found`)
}
if (!this.componentMap[category][component]) {
throw new Error(`Component "${component}" not found in category "${category}"`)
}
return this.componentMap[category][component]
}
list(category) {
return Object.keys(this.componentMap[category] || {})
}
listCategories() {
return Object.keys(this.componentMap)
}
}
// Usage
const resolver = new ComponentResolver(componentMap)
// In your component
const currentComponent = ref(resolver.resolve('user.Profile'))
2. Plugin Architecture
Create reusable plugin systems:
// plugins/index.js
import { developmentPlugin } from './development'
import { productionPlugin } from './production'
import { analyticsPlugin } from './analytics'
import { accessibilityPlugin } from './accessibility'
export class PluginManager {
constructor() {
this.plugins = new Map()
this.contexts = new WeakMap()
}
register(name, plugin) {
this.plugins.set(name, plugin)
}
createContext(environment = 'development') {
const plugins = []
// Environment-specific plugins
if (environment === 'development') {
plugins.push(developmentPlugin)
} else {
plugins.push(productionPlugin)
}
// Universal plugins
plugins.push(analyticsPlugin, accessibilityPlugin)
return plugins
}
get(name) {
return this.plugins.get(name)
}
}
// Environment-specific plugin configurations
export const createPluginConfig = (env) => {
const manager = new PluginManager()
// Register all available plugins
manager.register('development', developmentPlugin)
manager.register('production', productionPlugin)
manager.register('analytics', analyticsPlugin)
manager.register('accessibility', accessibilityPlugin)
return manager.createContext(env)
}
Testing Strategies
1. Component Testing
Test dynamic component behavior:
// tests/EwVueComponent.test.js
import { mount } from '@vue/test-utils'
import { EwVueComponent } from 'ew-vue-component'
import { describe, it, expect, vi } from 'vitest'
describe('EwVueComponent', () => {
it('should render string components', () => {
const wrapper = mount(EwVueComponent, {
props: { is: 'div' }
})
expect(wrapper.element.tagName).toBe('DIV')
})
it('should forward props correctly', () => {
const wrapper = mount(EwVueComponent, {
props: {
is: 'button',
disabled: true,
type: 'submit'
}
})
expect(wrapper.element.disabled).toBe(true)
expect(wrapper.element.type).toBe('submit')
})
it('should handle component loading errors', async () => {
const onError = vi.fn()
const wrapper = mount(EwVueComponent, {
props: {
is: 'NonExistentComponent'
},
listeners: {
error: onError
}
})
// Wait for error to be thrown
await wrapper.vm.$nextTick()
expect(onError).toHaveBeenCalled()
})
it('should use fallback when component fails', async () => {
const wrapper = mount(EwVueComponent, {
props: {
is: 'BrokenComponent',
fallback: 'div'
}
})
await wrapper.vm.$nextTick()
expect(wrapper.element.tagName).toBe('DIV')
})
it('should execute plugins in correct order', async () => {
const callOrder = []
const plugin1 = {
name: 'plugin1',
beforeRender: () => callOrder.push('plugin1-before'),
afterRender: () => callOrder.push('plugin1-after')
}
const plugin2 = {
name: 'plugin2',
beforeRender: () => callOrder.push('plugin2-before'),
afterRender: () => callOrder.push('plugin2-after')
}
mount(EwVueComponent, {
props: {
is: 'div',
plugins: [plugin1, plugin2]
}
})
await nextTick()
expect(callOrder).toEqual([
'plugin1-before',
'plugin2-before',
'plugin1-after',
'plugin2-after'
])
})
})
2. Integration Testing
Test component behavior in real scenarios:
// tests/integration/DynamicComponentApp.test.js
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import DynamicComponentApp from '@/components/DynamicComponentApp.vue'
describe('DynamicComponentApp Integration', () => {
let wrapper
beforeEach(() => {
wrapper = mount(DynamicComponentApp, {
global: {
plugins: [createTestingPinia()]
}
})
})
it('should switch between components correctly', async () => {
// Initial state
expect(wrapper.find('[data-testid="current-component"]').text())
.toBe('Dashboard')
// Switch to user profile
await wrapper.find('[data-testid="profile-button"]').trigger('click')
expect(wrapper.find('[data-testid="current-component"]').text())
.toBe('UserProfile')
// Verify component props are passed
expect(wrapper.find('[data-testid="user-name"]').text())
.toBe('John Doe')
})
it('should handle async component loading', async () => {
// Show loading state
await wrapper.find('[data-testid="reports-button"]').trigger('click')
expect(wrapper.find('[data-testid="loading"]').exists()).toBe(true)
// Wait for async component to load
await new Promise(resolve => setTimeout(resolve, 100))
await wrapper.vm.$nextTick()
expect(wrapper.find('[data-testid="loading"]').exists()).toBe(false)
expect(wrapper.find('[data-testid="reports-component"]').exists()).toBe(true)
})
})
Security Considerations
1. Component Validation
Validate components before rendering:
// utils/componentValidator.js
export class ComponentValidator {
constructor(options = {}) {
this.allowedTags = options.allowedTags || ['div', 'span', 'p', 'h1', 'h2', 'h3']
this.blockedComponents = options.blockedComponents || []
this.requireWhitelist = options.requireWhitelist || false
this.whitelistedComponents = options.whitelistedComponents || []
}
validate(component) {
// Check for blocked components
if (this.isBlocked(component)) {
throw new Error(`Component "${component}" is not allowed`)
}
// Check whitelist if required
if (this.requireWhitelist && !this.isWhitelisted(component)) {
throw new Error(`Component "${component}" is not in whitelist`)
}
// Validate string components (HTML tags)
if (typeof component === 'string') {
return this.validateTag(component)
}
// Validate component objects
if (typeof component === 'object') {
return this.validateComponentObject(component)
}
return true
}
isBlocked(component) {
const name = this.getComponentName(component)
return this.blockedComponents.includes(name)
}
isWhitelisted(component) {
const name = this.getComponentName(component)
return this.whitelistedComponents.includes(name)
}
validateTag(tag) {
if (!this.allowedTags.includes(tag)) {
throw new Error(`HTML tag "${tag}" is not allowed`)
}
return true
}
validateComponentObject(component) {
// Check for suspicious properties
const suspiciousProps = ['__proto__', 'constructor', 'prototype']
for (const prop of suspiciousProps) {
if (prop in component) {
throw new Error(`Suspicious property "${prop}" found in component`)
}
}
return true
}
getComponentName(component) {
if (typeof component === 'string') {
return component
}
if (typeof component === 'object' && component.name) {
return component.name
}
return 'Unknown'
}
}
// Usage with EwVueComponent
const validator = new ComponentValidator({
allowedTags: ['div', 'span', 'p', 'button', 'input'],
blockedComponents: ['ScriptComponent', 'UnsafeComponent'],
requireWhitelist: true,
whitelistedComponents: ['UserProfile', 'Dashboard', 'Settings']
})
const validationPlugin = {
name: 'validator',
beforeRender(component, props, context) {
try {
validator.validate(component)
} catch (error) {
context.utils.warn('Component validation failed:', error.message)
throw error
}
}
}
2. Secure Props Handling
Sanitize and validate props:
// utils/propsSanitizer.js
import DOMPurify from 'dompurify'
export class PropsSanitizer {
constructor(options = {}) {
this.sanitizeHtml = options.sanitizeHtml !== false
this.allowedEvents = options.allowedEvents || [
'onClick', 'onInput', 'onChange', 'onSubmit', 'onFocus', 'onBlur'
]
this.blockedProps = options.blockedProps || [
'innerHTML', 'outerHTML', 'dangerouslySetInnerHTML'
]
}
sanitize(props) {
const sanitized = {}
for (const [key, value] of Object.entries(props)) {
// Block dangerous props
if (this.blockedProps.includes(key)) {
console.warn(`Blocked dangerous prop: ${key}`)
continue
}
// Validate event handlers
if (key.startsWith('on') && !this.allowedEvents.includes(key)) {
console.warn(`Blocked event handler: ${key}`)
continue
}
// Sanitize HTML content
if (this.shouldSanitizeValue(key, value)) {
sanitized[key] = this.sanitizeValue(value)
} else {
sanitized[key] = value
}
}
return sanitized
}
shouldSanitizeValue(key, value) {
return this.sanitizeHtml &&
typeof value === 'string' &&
(key.includes('html') || key.includes('content'))
}
sanitizeValue(value) {
if (typeof value === 'string') {
return DOMPurify.sanitize(value)
}
return value
}
}
// Usage
const sanitizer = new PropsSanitizer()
const sanitizationPlugin = {
name: 'sanitizer',
beforeRender(component, props, context) {
const sanitizedProps = sanitizer.sanitize(props)
// Replace props with sanitized version
Object.assign(props, sanitizedProps)
}
}
Performance Monitoring
1. Real-time Performance Tracking
Monitor component performance in production:
// utils/performanceMonitor.js
export class PerformanceMonitor {
constructor(options = {}) {
this.enabled = options.enabled !== false
this.slowThreshold = options.slowThreshold || 100
this.verySlowThreshold = options.verySlowThreshold || 500
this.reportingEndpoint = options.reportingEndpoint
this.sampleRate = options.sampleRate || 0.1 // 10% sampling
}
measureComponent(componentName, measureFn) {
if (!this.enabled || !this.shouldSample()) {
return measureFn()
}
const startTime = performance.now()
const startMark = `${componentName}-start`
const endMark = `${componentName}-end`
performance.mark(startMark)
try {
const result = measureFn()
if (result && typeof result.then === 'function') {
// Handle async components
return result.then(
(value) => {
this.finalizeMeasurement(componentName, startTime, startMark, endMark)
return value
},
(error) => {
this.finalizeMeasurement(componentName, startTime, startMark, endMark, error)
throw error
}
)
} else {
this.finalizeMeasurement(componentName, startTime, startMark, endMark)
return result
}
} catch (error) {
this.finalizeMeasurement(componentName, startTime, startMark, endMark, error)
throw error
}
}
finalizeMeasurement(componentName, startTime, startMark, endMark, error = null) {
const endTime = performance.now()
const duration = endTime - startTime
performance.mark(endMark)
performance.measure(componentName, startMark, endMark)
const performanceData = {
component: componentName,
duration,
timestamp: new Date().toISOString(),
error: error ? error.message : null,
slow: duration > this.slowThreshold,
verySlow: duration > this.verySlowThreshold
}
// Log slow components
if (duration > this.slowThreshold) {
console.warn(`Slow component detected: ${componentName} (${duration.toFixed(2)}ms)`)
}
// Report to analytics
this.reportPerformance(performanceData)
}
shouldSample() {
return Math.random() < this.sampleRate
}
async reportPerformance(data) {
if (!this.reportingEndpoint) return
try {
await fetch(this.reportingEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
} catch (error) {
console.warn('Failed to report performance data:', error)
}
}
}
// Usage
const monitor = new PerformanceMonitor({
slowThreshold: 100,
verySlowThreshold: 500,
reportingEndpoint: '/api/performance',
sampleRate: 0.05 // 5% sampling in production
})
export { monitor }
This comprehensive best practices guide covers the most important aspects of using EwVueComponent effectively, including performance optimization, error handling, code organization, testing, security, and monitoring. Following these practices will help you build robust, performant, and maintainable Vue 3 applications.