- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
23 KiB
LiveComponents Best Practices
Production-ready patterns and best practices for building scalable, maintainable LiveComponents.
Table of Contents
- Component Design
- State Management
- Performance Optimization
- Security Best Practices
- Testing Strategies
- Error Handling
- Accessibility
- Production Deployment
Component Design
Single Responsibility Principle
✅ Good: Component does one thing well
// Focused component
final class SearchBox extends LiveComponent
{
#[LiveProp(writable: true)]
public string $query = '';
#[LiveProp]
public array $suggestions = [];
#[LiveAction(debounce: 300)]
public function search(): void
{
$this->suggestions = $this->searchService->getSuggestions($this->query);
}
}
❌ Bad: Component tries to do everything
// God component (anti-pattern)
final class Dashboard extends LiveComponent
{
// User management
#[LiveAction]
public function createUser() { }
#[LiveAction]
public function deleteUser() { }
// Analytics
#[LiveAction]
public function generateReport() { }
// Settings
#[LiveAction]
public function updateSettings() { }
// Notifications
#[LiveAction]
public function sendNotification() { }
// ... 50 more actions
}
Solution: Split into focused components
final class UserManagement extends LiveComponent { }
final class AnalyticsDashboard extends LiveComponent { }
final class SettingsPanel extends LiveComponent { }
final class NotificationCenter extends LiveComponent { }
Component Composition
✅ Good: Compose components from smaller pieces
<div data-lc-component="Dashboard">
<include template="components/header" />
<div class="grid">
<include template="components/user-stats" data-lc-component="UserStats" />
<include template="components/activity-feed" data-lc-component="ActivityFeed" />
<include template="components/notifications" data-lc-component="Notifications" />
</div>
</div>
Benefits:
- Reusable components
- Easier testing
- Better performance (independent updates)
- Clearer separation of concerns
Keep Components Stateless When Possible
✅ Good: Stateless, reusable component
final class UserCard extends LiveComponent
{
#[LiveProp]
public string $userId;
#[LiveProp]
public string $name;
#[LiveProp]
public string $avatar;
// No mutable state, just display props
}
❌ Bad: Unnecessary internal state
final class UserCard extends LiveComponent
{
#[LiveProp]
public string $userId;
#[LiveProp]
private bool $isExpanded = false; // Could be CSS-only
#[LiveProp]
private int $hoverCount = 0; // Useless tracking
}
Use Value Objects
✅ Good: Type-safe with Value Objects
final class OrderForm extends LiveComponent
{
#[LiveProp]
public Money $total;
#[LiveProp]
public Address $shippingAddress;
#[LiveProp]
public OrderStatus $status;
}
❌ Bad: Primitive obsession
final class OrderForm extends LiveComponent
{
#[LiveProp]
public float $total; // No currency info
#[LiveProp]
public array $shippingAddress; // Unstructured
#[LiveProp]
public string $status; // No validation
}
State Management
Minimize State
✅ Good: Only essential state
final class ProductList extends LiveComponent
{
#[LiveProp]
public array $filters = []; // User input
#[LiveProp]
public array $products = []; // Query results
// Everything else computed from these two
}
❌ Bad: Redundant state
final class ProductList extends LiveComponent
{
#[LiveProp]
public array $filters = [];
#[LiveProp]
public array $products = [];
#[LiveProp]
public int $totalProducts; // Redundant: count($products)
#[LiveProp]
public bool $hasProducts; // Redundant: !empty($products)
#[LiveProp]
public array $productIds; // Redundant: array_column($products, 'id')
}
Computed Properties for Derived State
✅ Good: Compute derived values
final class ShoppingCart extends LiveComponent
{
#[LiveProp]
public array $items = [];
#[LiveProp(computed: true)]
public function subtotal(): Money
{
return array_reduce(
$this->items,
fn($sum, $item) => $sum->add($item->price->multiply($item->quantity)),
Money::zero()
);
}
#[LiveProp(computed: true)]
public function itemCount(): int
{
return array_sum(array_column($this->items, 'quantity'));
}
}
Normalize Complex State
✅ Good: Normalized state
final class MessageList extends LiveComponent
{
#[LiveProp]
public array $messageIds = [1, 2, 3]; // Order preserved
#[LiveProp]
public array $messagesById = [ // Fast lookup
1 => ['id' => 1, 'text' => '...'],
2 => ['id' => 2, 'text' => '...'],
3 => ['id' => 3, 'text' => '...'],
];
#[LiveAction]
public function deleteMessage(int $id): void
{
$this->messageIds = array_filter($this->messageIds, fn($msgId) => $msgId !== $id);
unset($this->messagesById[$id]);
}
}
❌ Bad: Nested arrays
final class MessageList extends LiveComponent
{
#[LiveProp]
public array $messages = [
['id' => 1, 'text' => '...'],
['id' => 2, 'text' => '...'],
['id' => 3, 'text' => '...'],
];
#[LiveAction]
public function deleteMessage(int $id): void
{
// Slow O(n) search
$this->messages = array_filter(
$this->messages,
fn($msg) => $msg['id'] !== $id
);
}
}
Use Writable Props Sparingly
✅ Good: Writable only for user input
final class ContactForm extends LiveComponent
{
#[LiveProp(writable: true)]
public string $name = '';
#[LiveProp(writable: true)]
public string $email = '';
#[LiveProp] // Read-only
public bool $submitted = false;
}
❌ Bad: Everything writable
final class ContactForm extends LiveComponent
{
#[LiveProp(writable: true)]
public string $name = '';
#[LiveProp(writable: true)]
public bool $submitted = false; // Client could fake this
#[LiveProp(writable: true)]
public string $userId; // Security risk!
}
Performance Optimization
Use Fragments for Large Components
✅ Good: Fragment-based updates
final class Dashboard extends LiveComponent
{
#[LiveAction]
#[Fragment('stats')]
public function refreshStats(): void
{
// Only re-renders stats section
$this->stats = $this->statsService->getLatest();
}
#[LiveAction]
#[Fragment('activity')]
public function refreshActivity(): void
{
// Only re-renders activity feed
$this->activities = $this->activityService->getRecent();
}
}
Performance Impact: 70-90% reduction in DOM updates
Debounce Expensive Actions
✅ Good: Debounced search
final class SearchBox extends LiveComponent
{
#[LiveProp(writable: true)]
public string $query = '';
#[LiveAction(debounce: 300)]
public function search(): void
{
// Waits 300ms after user stops typing
$this->results = $this->searchService->search($this->query);
}
}
Performance Impact: 80%+ reduction in API calls
Cache Expensive Operations
✅ Good: Cached expensive query
final class ReportViewer extends LiveComponent
{
#[LiveAction]
#[Cached(ttl: 1800, tags: ['reports'])]
public function generateReport(): void
{
// Expensive operation cached for 30 minutes
$this->reportData = $this->reportService->generate($this->reportId);
}
}
Lazy Load Non-Critical Data
✅ Good: Lazy loading
final class UserProfile extends LiveComponent
{
#[LiveProp]
public string $userId;
#[LiveProp(lazy: true)]
public array $activityHistory = [];
#[LiveAction]
public function loadActivityHistory(): void
{
// Only loads when user requests it
$this->activityHistory = $this->activityService->getForUser($this->userId);
}
}
Optimize Template Rendering
✅ Good: Efficient template
<!-- Use fragments for independent sections -->
<div data-lc-component="ProductList">
<div data-lc-fragment="filters">
<!-- Filter UI -->
</div>
<div data-lc-fragment="products">
<!-- Only this updates when products change -->
<for items="products" as="product">
<include template="components/product-card" data="{{ product }}" />
</for>
</div>
</div>
❌ Bad: Monolithic template
<!-- Everything re-renders on any change -->
<div data-lc-component="ProductList">
<!-- 10KB of HTML -->
<div><!-- Filters --></div>
<div><!-- Products --></div>
<div><!-- Sidebar --></div>
<div><!-- Footer --></div>
</div>
Use Optimistic Updates for User Interactions
✅ Good: Instant feedback
final class LikeButton extends LiveComponent
{
#[LiveAction]
#[Optimistic(updateProps: ['likes', 'isLiked'])]
public function toggleLike(): void
{
// Client updates UI immediately (<50ms)
// Server confirms asynchronously
$this->isLiked = !$this->isLiked;
$this->likes += $this->isLiked ? 1 : -1;
}
}
Security Best Practices
Rate Limit All Public Actions
✅ Good: Protected endpoints
final class ContactForm extends LiveComponent
{
#[LiveAction]
#[RateLimit(requests: 3, window: 3600, key: 'ip')]
public function submitForm(array $data): void
{
// Max 3 submissions per hour per IP
$this->emailService->send($data);
}
}
Authorize Sensitive Actions
✅ Good: Protected actions
final class UserManagement extends LiveComponent
{
#[LiveAction]
#[Authorize(roles: ['admin'])]
#[RateLimit(requests: 10, window: 60, key: 'user')]
public function deleteUser(string $userId): void
{
// Only admins can delete, max 10 per minute
$this->userService->delete($userId);
}
}
Use Idempotency for Critical Actions
✅ Good: Idempotent operations
final class PaymentForm extends LiveComponent
{
#[LiveAction]
#[Idempotent(ttl: 86400)]
#[Authorize(permissions: ['payments.process'])]
public function processPayment(string $idempotencyKey, array $paymentData): void
{
// Duplicate requests return cached result
// Prevents double-charging
$this->paymentService->charge($paymentData);
}
}
Validate All Input
✅ Good: Server-side validation
final class RegistrationForm extends LiveComponent
{
#[LiveAction]
#[Validated(rules: [
'email' => ['required', 'email', 'unique:users'],
'password' => ['required', 'min:8', 'confirmed'],
])]
#[RateLimit(requests: 5, window: 3600, key: 'ip')]
public function register(string $email, string $password): void
{
// Input validated before execution
$this->authService->register($email, $password);
}
}
Encrypt Sensitive Data
✅ Good: Encrypted sensitive props
final class PaymentForm extends LiveComponent
{
#[LiveProp]
#[Encrypted]
public string $creditCardNumber = '';
#[LiveProp]
#[Encrypted]
public string $cvv = '';
#[LiveAction]
#[Idempotent]
#[Authorize(permissions: ['payments.process'])]
public function processPayment(): void
{
// Properties automatically decrypted server-side
$this->paymentService->charge($this->creditCardNumber, $this->cvv);
}
}
Never Trust Client State
✅ Good: Validate server-side
final class CheckoutForm extends LiveComponent
{
#[LiveProp(writable: true)]
public array $cartItems = [];
#[LiveAction]
public function checkout(): void
{
// ALWAYS recalculate totals server-side
$total = $this->cartService->calculateTotal($this->cartItems);
// Don't trust client-provided total
$this->paymentService->charge($total);
}
}
❌ Bad: Trust client state
final class CheckoutForm extends LiveComponent
{
#[LiveProp(writable: true)]
public float $total; // Client could modify this!
#[LiveAction]
public function checkout(): void
{
// Dangerous: charging client-provided amount
$this->paymentService->charge($this->total);
}
}
Testing Strategies
Unit Test Component Logic
✅ Good: Test business logic
it('calculates cart total correctly', function () {
$cart = new ShoppingCart();
$cart->items = [
new CartItem(price: Money::fromCents(1000), quantity: 2),
new CartItem(price: Money::fromCents(500), quantity: 1),
];
expect($cart->subtotal()->toCents())->toBe(2500);
});
Integration Test Actions
✅ Good: Test action execution
it('adds item to cart', function () {
$cart = new ShoppingCart();
$cart->addItem(productId: '123', quantity: 2);
expect($cart->items)->toHaveCount(1);
expect($cart->items[0]->productId)->toBe('123');
expect($cart->items[0]->quantity)->toBe(2);
});
E2E Test User Workflows
✅ Good: Test complete flows
test('user can complete checkout', async ({ page }) => {
// Add product to cart
await page.goto('/products/123');
await page.click('[data-lc-action="addToCart"]');
// Go to cart
await page.goto('/cart');
await expect(page.locator('.cart-item')).toHaveCount(1);
// Proceed to checkout
await page.click('[data-lc-action="checkout"]');
// Fill payment form
await page.fill('[name="cardNumber"]', '4242424242424242');
await page.fill('[name="cvv"]', '123');
// Submit
await page.click('[data-lc-action="processPayment"]');
// Verify success
await expect(page).toHaveURL('/order/confirmation');
});
Test Fragment Updates
✅ Good: Verify partial rendering
test('updates only search results fragment', async ({ page }) => {
const headerHtml = await page.locator('[data-lc-fragment="header"]').innerHTML();
await page.fill('[data-lc-model="searchQuery"]', 'test');
await page.click('[data-lc-action="search"]');
await page.waitForResponse(r => r.url().includes('/livecomponent'));
// Header unchanged
const newHeaderHtml = await page.locator('[data-lc-fragment="header"]').innerHTML();
expect(newHeaderHtml).toBe(headerHtml);
// Results updated
await expect(page.locator('[data-lc-fragment="results"] .result-item')).toHaveCount(5);
});
Test Error Handling
✅ Good: Test failure scenarios
it('handles validation errors', function () {
$form = new RegistrationForm();
expect(fn() => $form->register('invalid-email', 'short'))
->toThrow(ValidationException::class);
expect($form->errors)->toHaveKey('email');
expect($form->errors)->toHaveKey('password');
});
Error Handling
Provide Clear Error Messages
✅ Good: User-friendly errors
final class PaymentForm extends LiveComponent
{
#[LiveAction]
public function processPayment(array $paymentData): void
{
try {
$this->paymentService->charge($paymentData);
$this->success = true;
} catch (InsufficientFundsException $e) {
$this->error = 'Insufficient funds. Please try a different payment method.';
} catch (CardDeclinedException $e) {
$this->error = 'Your card was declined. Please contact your bank.';
} catch (PaymentGatewayException $e) {
$this->error = 'Payment processing is temporarily unavailable. Please try again later.';
$this->logger->error('Payment gateway error', ['exception' => $e]);
}
}
}
❌ Bad: Technical error messages
catch (\Exception $e) {
$this->error = $e->getMessage(); // "SQLSTATE[HY000]..."
}
Handle Network Failures Gracefully
✅ Good: Retry with backoff
window.addEventListener('livecomponent:request-failed', async (event) => {
const { componentId, requestId, error } = event.detail;
if (error.status === 0) {
// Network error - retry with exponential backoff
let delay = 1000;
for (let i = 0; i < 3; i++) {
await new Promise(resolve => setTimeout(resolve, delay));
try {
await retryRequest(componentId, requestId);
break; // Success
} catch (e) {
delay *= 2;
}
}
}
});
Validate Input Early
✅ Good: Client-side + server-side validation
<form data-lc-action="register">
<input
type="email"
name="email"
required
pattern="[^@]+@[^@]+\.[^@]+"
data-lc-model="email"
/>
<input
type="password"
name="password"
required
minlength="8"
data-lc-model="password"
/>
<button type="submit">Register</button>
</form>
#[LiveAction]
#[Validated(rules: [
'email' => ['required', 'email', 'unique:users'],
'password' => ['required', 'min:8'],
])]
public function register(string $email, string $password): void
{
// Server-side validation happens first
$this->authService->register($email, $password);
}
Accessibility
Use Semantic HTML
✅ Good: Semantic markup
<nav aria-label="Main navigation" data-lc-component="Navigation">
<ul role="list">
<for items="menuItems" as="item">
<li>
<a
href="{{ item.url }}"
aria-current="{{ item.isActive ? 'page' : null }}"
>
{{ item.label }}
</a>
</li>
</for>
</ul>
</nav>
❌ Bad: Divs everywhere
<div data-lc-component="Navigation">
<div onclick="navigate()">Home</div>
<div onclick="navigate()">About</div>
</div>
Provide ARIA Labels
✅ Good: Accessible labels
<button
data-lc-action="deleteItem"
data-lc-params='{"id": "{{ item.id }}"}'
aria-label="Delete {{ item.name }}"
>
✕
</button>
<div
data-lc-fragment="loading"
role="status"
aria-live="polite"
aria-busy="{{ isLoading }}"
>
<if condition="isLoading">
Loading...
</if>
</div>
Announce Dynamic Updates
✅ Good: Screen reader announcements
<div role="status" aria-live="polite" aria-atomic="true">
<if condition="successMessage">
{{ successMessage }}
</if>
</div>
<div role="alert" aria-live="assertive">
<if condition="errorMessage">
{{ errorMessage }}
</if>
</div>
Keyboard Navigation
✅ Good: Keyboard accessible
<div
data-lc-component="Dropdown"
role="combobox"
aria-expanded="{{ isOpen }}"
aria-controls="dropdown-list"
>
<button
data-lc-action="toggle"
aria-haspopup="listbox"
onkeydown="handleKeyDown(event)"
>
{{ selectedOption }}
</button>
<ul
id="dropdown-list"
role="listbox"
aria-label="Options"
style="display: {{ isOpen ? 'block' : 'none' }}"
>
<for items="options" as="option">
<li
role="option"
tabindex="0"
data-lc-action="selectOption"
data-lc-params='{"value": "{{ option.value }}"}'
onkeydown="handleOptionKeyDown(event)"
>
{{ option.label }}
</li>
</for>
</ul>
</div>
Production Deployment
Environment Configuration
✅ Production .env:
# LiveComponents Configuration
LIVECOMPONENT_ENABLED=true
LIVECOMPONENT_CSRF_PROTECTION=true
LIVECOMPONENT_RATE_LIMIT=60
LIVECOMPONENT_DEVTOOLS_ENABLED=false
# Security
LIVECOMPONENT_ENCRYPTION_KEY=your-256-bit-encryption-key-here
CSRF_TOKEN_LENGTH=32
# Performance
LIVECOMPONENT_BATCH_SIZE=10
LIVECOMPONENT_BATCH_DEBOUNCE=50
LIVECOMPONENT_CACHE_ENABLED=true
# SSE
LIVECOMPONENT_SSE_ENABLED=true
LIVECOMPONENT_SSE_HEARTBEAT_INTERVAL=30
Production Checklist
Before Deploying:
- Generate new
LIVECOMPONENT_ENCRYPTION_KEY - Disable DevTools (
LIVECOMPONENT_DEVTOOLS_ENABLED=false) - Enable CSRF protection
- Configure rate limits for all public actions
- Test all critical user flows
- Run security audit
- Test with real-world data volumes
- Configure caching
- Set up error monitoring
- Test SSE connections (HTTPS required)
- Verify fragment updates work correctly
- Test file uploads with large files
- Test error handling and recovery
- Validate accessibility (WCAG 2.1 AA)
Monitor Performance
✅ Good: Track metrics
window.addEventListener('livecomponent:action-executed', (event) => {
const { action, duration } = event.detail;
// Send to analytics
analytics.track('livecomponent_performance', {
action: action,
duration: duration,
p95: calculateP95(action, duration),
p99: calculateP99(action, duration)
});
// Alert on slow actions
if (duration > 1000) {
errorReporter.captureMessage(`Slow action: ${action} (${duration}ms)`);
}
});
Error Tracking
✅ Good: Comprehensive error tracking
window.addEventListener('livecomponent:error', (event) => {
const { componentId, error, context } = event.detail;
errorReporter.captureException(error, {
tags: {
component: componentId,
context: context
},
extra: {
props: LiveComponent.getComponent(componentId)?.props,
user_agent: navigator.userAgent
}
});
});
Performance Budgets
Set and Monitor:
- Action latency: p95 < 100ms, p99 < 250ms
- Fragment updates: < 50ms
- Network efficiency: > 80% (via batching)
- Memory usage: < 2MB per component
- Bundle size: < 50KB (gzipped)
Summary: The Golden Rules
- Keep components focused - Single responsibility
- Minimize state - Compute derived values
- Use fragments - For large components (>5KB)
- Debounce expensive actions - Search, API calls
- Cache when possible - Expensive queries
- Rate limit everything - Public endpoints
- Authorize sensitive actions - Admin operations
- Validate all input - Client + server
- Encrypt sensitive data - PII, financial
- Test thoroughly - Unit, integration, E2E
- Handle errors gracefully - User-friendly messages
- Make it accessible - WCAG 2.1 AA
- Monitor production - Performance + errors
- Use optimistic updates - Instant feedback
- Document your code - Future you will thank you
For more details, see: