- 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
Advanced LiveComponents Features
Deep Dive into Advanced LiveComponents Functionality
This guide explores advanced features for building sophisticated, high-performance LiveComponents applications.
Table of Contents
- Fragment-Based Rendering
- Request Batching
- Server-Sent Events (SSE)
- Optimistic UI Updates
- Chunked File Uploads
- Component Communication
- State Management Patterns
- Custom Actions
Fragment-Based Rendering
Overview
Fragment rendering enables updating specific parts of a component without re-rendering the entire component DOM tree.
Benefits:
- 70-90% reduction in DOM updates
- Preserved focus and scroll position
- Improved perceived performance
- Lower bandwidth usage
Basic Fragment Usage
<!-- Component template with fragments -->
<div data-component-id="{component_id}">
<!-- Static header - never updates -->
<header>
<h1>Product Dashboard</h1>
</header>
<!-- Fragment 1: Product list -->
<div data-lc-fragment="product-list">
<for items="products" as="product">
<div class="product-card">
<h3>{product.name}</h3>
<p>{product.price}</p>
</div>
</for>
</div>
<!-- Fragment 2: Shopping cart -->
<div data-lc-fragment="cart">
<p>Items: {cartCount}</p>
<p>Total: {cartTotal}</p>
</div>
<!-- Fragment 3: User preferences -->
<div data-lc-fragment="preferences">
<!-- Rarely updated -->
<form data-lc-submit="savePreferences">
<!-- Form fields -->
</form>
</div>
</div>
Server-Side Fragment Control
use App\Framework\LiveComponents\Attributes\Fragment;
final class ProductDashboard extends LiveComponent
{
#[LiveProp]
public array $products = [];
#[LiveProp]
public int $cartCount = 0;
#[LiveProp]
public float $cartTotal = 0.0;
// Update only product list fragment
#[LiveAction]
#[Fragment('product-list')]
public function filterProducts(string $category): void
{
$this->products = $this->productService->filterByCategory($category);
// Cart and preferences remain unchanged
}
// Update only cart fragment
#[LiveAction]
#[Fragment('cart')]
public function addToCart(string $productId): void
{
$this->cart->add($productId);
$this->cartCount = $this->cart->count();
$this->cartTotal = $this->cart->total();
// Product list and preferences remain unchanged
}
// Update multiple fragments
#[LiveAction]
#[Fragment(['product-list', 'cart'])]
public function clearCart(): void
{
$this->cart->clear();
$this->cartCount = 0;
$this->cartTotal = 0.0;
$this->loadRecommendedProducts(); // Affects product list
}
}
Nested Fragments
<!-- Nested fragment pattern -->
<div data-lc-fragment="dashboard">
<div class="header">
<h2>Analytics Dashboard</h2>
</div>
<!-- Child fragment 1 -->
<div data-lc-fragment="user-stats">
<p>Active Users: {activeUsers}</p>
<p>New Signups: {newSignups}</p>
</div>
<!-- Child fragment 2 -->
<div data-lc-fragment="revenue-stats">
<p>Today's Revenue: {todayRevenue}</p>
<p>Monthly Revenue: {monthlyRevenue}</p>
</div>
</div>
// Update parent fragment (updates everything)
#[LiveAction]
#[Fragment('dashboard')]
public function refreshAll(): void
{
$this->updateUserStats();
$this->updateRevenueStats();
}
// Update specific child fragment
#[LiveAction]
#[Fragment('user-stats')]
public function refreshUserStats(): void
{
$this->activeUsers = $this->analyticsService->getActiveUsers();
$this->newSignups = $this->analyticsService->getNewSignups();
}
Dynamic Fragment Names
<!-- Dynamic fragments per item -->
<div class="todo-list">
<for items="todos" as="todo">
<div data-lc-fragment="todo-{todo.id}" class="todo-item">
<input
type="checkbox"
{todo.completed ? 'checked' : ''}
data-lc-action="toggleTodo"
data-lc-params='{"id": "{todo.id}"}'
/>
<span>{todo.title}</span>
</div>
</for>
</div>
#[LiveAction]
#[Fragment] // Fragment name computed dynamically
public function toggleTodo(string $id): void
{
$todo = $this->findTodo($id);
$todo->toggle();
// Set fragment name dynamically
$this->setFragmentName("todo-{$id}");
}
Fragment Events
// Listen for fragment updates
window.addEventListener('livecomponent:fragment-updated', (e) => {
const { fragmentName, duration, nodesChanged } = e.detail;
console.log(`Fragment "${fragmentName}" updated:`);
console.log(`- Duration: ${duration}ms`);
console.log(`- Nodes changed: ${nodesChanged}`);
// Trigger custom logic after specific fragment updates
if (fragmentName === 'cart') {
updateCartBadge();
}
});
Request Batching
Overview
Request batching automatically combines multiple rapid actions into a single HTTP request for network efficiency.
Benefits:
- 80-95% reduction in HTTP requests
- Lower server load
- Better mobile performance
- Reduced latency
Automatic Batching
// Configure batching globally
LiveComponent.configure({
batchSize: 10, // Max actions per batch
batchDebounce: 50 // Wait 50ms for more actions
});
How it works:
User clicks button 1 → Queue action
User clicks button 2 → Queue action } 50ms debounce window
User clicks button 3 → Queue action
↓
Single batched request with 3 actions
Manual Batch Control
// Manually flush batch immediately
LiveComponent.flushBatch('component-id');
// Pause batching temporarily
LiveComponent.pauseBatching();
// Resume batching
LiveComponent.resumeBatching();
Batch-Aware Action Design
// Actions designed for batching efficiency
final class ProductGrid extends LiveComponent
{
#[LiveAction]
public function quickView(string $productId): void
{
// Fast, stateless - perfect for batching
$this->activeProductId = $productId;
}
#[LiveAction]
public function addToWishlist(string $productId): void
{
// Independent operation - batches well
$this->wishlist->add($productId);
}
#[LiveAction]
public function updateFilters(array $filters): void
{
// Depends on state - still batches, processed in order
$this->filters = $filters;
$this->applyFilters();
}
}
Batch Response Handling
// Monitor batch efficiency
window.addEventListener('livecomponent:batch-sent', (e) => {
const { actionsCount, payloadSize } = e.detail;
const requestsSaved = actionsCount - 1;
const efficiency = (requestsSaved / actionsCount) * 100;
console.log(`Batch sent: ${actionsCount} actions`);
console.log(`Requests saved: ${requestsSaved}`);
console.log(`Efficiency: ${efficiency.toFixed(1)}%`);
});
// Handle batch response
window.addEventListener('livecomponent:batch-completed', (e) => {
const { results, duration } = e.detail;
// Process individual action results
results.forEach((result, index) => {
if (!result.success) {
console.error(`Action ${index} failed:`, result.error);
}
});
});
Conditional Batching
use App\Framework\LiveComponents\Attributes\NoBatch;
final class CriticalAction extends LiveComponent
{
// Disable batching for critical actions
#[LiveAction]
#[NoBatch]
public function processPayment(): void
{
// Always sent as standalone request
$this->paymentService->charge($this->amount);
}
// Allow batching (default)
#[LiveAction]
public function updateQuantity(string $productId, int $qty): void
{
$this->cart->updateQuantity($productId, $qty);
}
}
Server-Sent Events (SSE)
Overview
Server-Sent Events enable real-time, server-to-client updates without polling.
Benefits:
- Real-time updates without polling
- Lower bandwidth than WebSockets for one-way communication
- Automatic reconnection
- HTTP/2 multiplexing support
Basic SSE Usage
final class LiveDashboard extends LiveComponent
{
#[LiveProp]
public int $activeUsers = 0;
#[LiveProp]
public array $recentActivity = [];
public function mount(): void
{
// Enable SSE for this component
$this->enableSse();
// Subscribe to specific channels
$this->subscribeTo([
'user.activity',
'system.notifications',
"team.{$this->teamId}.updates"
]);
}
public function unmount(): void
{
// Clean up SSE connection
$this->disableSse();
}
}
Broadcasting Updates
use App\Framework\LiveComponents\SSE\SseBroadcaster;
final class UserActivityService
{
public function __construct(
private readonly SseBroadcaster $broadcaster
) {}
public function recordLogin(User $user): void
{
$this->repository->recordLogin($user);
// Broadcast to all listening components
$this->broadcaster->broadcast('user.activity', [
'type' => 'login',
'user_id' => $user->id,
'username' => $user->name,
'timestamp' => time()
]);
}
}
Client-Side SSE Handling
// Listen for SSE updates
window.addEventListener('livecomponent:sse-message', (e) => {
const { channel, data } = e.detail;
console.log(`SSE message on "${channel}":`, data);
// Handle specific event types
if (data.type === 'login') {
showNotification(`${data.username} logged in`);
}
});
// Connection status
window.addEventListener('livecomponent:sse-connected', (e) => {
console.log('SSE connected:', e.detail.componentId);
showConnectionStatus('connected');
});
window.addEventListener('livecomponent:sse-disconnected', (e) => {
console.warn('SSE disconnected:', e.detail.reason);
showConnectionStatus('disconnected');
});
SSE with Automatic Component Updates
// Server automatically triggers component re-render
public function broadcastUpdate(string $message): void
{
$this->broadcaster->broadcast("component.{$this->id}.update", [
'trigger_render' => true, // Automatic re-render
'message' => $message
]);
}
Connection Pooling
use App\Framework\LiveComponents\SSE\SseConnectionPool;
final class SseOptimizedComponent extends LiveComponent
{
public function mount(): void
{
// Reuse existing connection pool
$this->ssePool->subscribe(
componentId: $this->id,
channels: ['global.updates']
);
}
}
Optimistic UI Updates
Overview
Optimistic UI updates the interface immediately before server confirmation for instant perceived performance.
Benefits:
- <50ms perceived latency
- Better user experience
- Higher engagement
- Network-independent responsiveness
Simple Optimistic Updates
<!-- Optimistic toggle -->
<button
data-lc-action="toggleLike"
data-optimistic="true"
>
{isLiked ? '❤️ Liked' : '🤍 Like'}
</button>
<!-- Optimistic counter -->
<button
data-lc-action="incrementCounter"
data-optimistic="increment"
>
Count: {count}
</button>
Server-Side Optimistic Configuration
use App\Framework\LiveComponents\Attributes\Optimistic;
final class PostInteractions extends LiveComponent
{
#[LiveProp]
public bool $isLiked = false;
#[LiveProp]
public int $likeCount = 0;
#[LiveAction]
#[Optimistic(property: 'isLiked', operation: 'toggle')]
#[Optimistic(property: 'likeCount', operation: 'increment')]
public function toggleLike(): void
{
// Client already updated UI optimistically
// Server just persists the change
$this->isLiked = !$this->isLiked;
$this->likeCount += $this->isLiked ? 1 : -1;
$this->postService->toggleLike($this->postId, $this->isLiked);
}
}
Custom Optimistic Operations
#[LiveAction]
#[Optimistic(property: 'status', operation: 'custom', handler: 'optimisticStatusChange')]
public function changeStatus(string $newStatus): void
{
$this->status = $newStatus;
$this->statusService->update($this->id, $newStatus);
}
// Custom optimistic handler
public function optimisticStatusChange(string $newStatus): mixed
{
// Return optimistic value
return match($newStatus) {
'pending' => 'Processing...',
'approved' => 'Approved ✓',
'rejected' => 'Rejected ✗',
default => $newStatus
};
}
Error Handling & Rollback
use App\Framework\LiveComponents\Exceptions\OptimisticConflictException;
#[LiveAction]
#[Optimistic(property: 'quantity', operation: 'set')]
public function updateQuantity(int $newQuantity): void
{
// Check for conflicts
$currentStock = $this->inventoryService->getStock($this->productId);
if ($newQuantity > $currentStock) {
// Rollback optimistic update
throw new OptimisticConflictException(
"Insufficient stock. Available: {$currentStock}"
);
}
$this->quantity = $newQuantity;
$this->cartService->updateQuantity($this->productId, $newQuantity);
}
Client-Side Rollback Handling
// Handle optimistic rollback
window.addEventListener('livecomponent:optimistic-rollback', (e) => {
const { action, error, stateBeforeOptimistic } = e.detail;
// Show error to user
showNotification(error, 'error');
// Optional: Custom recovery
if (action === 'updateQuantity') {
highlightField('quantity-input');
}
});
Conditional Optimistic Updates
#[LiveAction]
#[Optimistic(property: 'votes', operation: 'increment', condition: 'canVote')]
public function upvote(): void
{
if (!$this->canVote()) {
throw new UnauthorizedException('Already voted');
}
$this->votes++;
$this->voteService->record($this->postId, $this->userId);
}
private function canVote(): bool
{
return !$this->hasVoted($this->userId);
}
Chunked File Uploads
Overview
Chunked uploads split large files into smaller pieces for reliable, resumable uploads with progress tracking.
Benefits:
- Upload large files (GB+)
- Pause/resume support
- Progress tracking
- Retry failed chunks
- Validation before upload
Basic Upload Configuration
<form>
<input
type="file"
data-lc-upload="handleFileUpload"
data-chunk-size="1048576"
data-max-file-size="104857600"
accept=".pdf,.jpg,.png"
/>
<div class="upload-progress" data-upload-progress>
<div class="progress-bar" style="width: {uploadProgress}%"></div>
<span>{uploadProgress}%</span>
<button data-lc-action="pauseUpload">Pause</button>
<button data-lc-action="resumeUpload">Resume</button>
<button data-lc-action="cancelUpload">Cancel</button>
</div>
</form>
Server-Side Upload Handling
use App\Framework\LiveComponents\Attributes\LiveAction;
use App\Framework\LiveComponents\Upload\ChunkUploadHandler;
final class FileUploadComponent extends LiveComponent
{
#[LiveProp]
public float $uploadProgress = 0.0;
#[LiveProp]
public ?string $uploadId = null;
#[LiveProp]
public bool $uploadPaused = false;
#[LiveAction]
public function handleFileUpload(
string $uploadId,
int $chunkIndex,
int $totalChunks,
string $filename,
string $chunkData
): void {
$this->uploadId = $uploadId;
// Store chunk
$this->chunkUploadHandler->storeChunk(
uploadId: $uploadId,
chunkIndex: $chunkIndex,
data: base64_decode($chunkData)
);
// Update progress
$this->uploadProgress = ($chunkIndex + 1) / $totalChunks * 100;
// Last chunk - assemble file
if ($chunkIndex === $totalChunks - 1) {
$finalPath = $this->chunkUploadHandler->assembleFile(
uploadId: $uploadId,
filename: $filename,
totalChunks: $totalChunks
);
$this->processUploadedFile($finalPath);
}
}
#[LiveAction]
public function pauseUpload(): void
{
$this->uploadPaused = true;
}
#[LiveAction]
public function resumeUpload(): void
{
$this->uploadPaused = false;
}
#[LiveAction]
public function cancelUpload(): void
{
if ($this->uploadId) {
$this->chunkUploadHandler->cleanup($this->uploadId);
$this->uploadId = null;
$this->uploadProgress = 0.0;
}
}
}
Client-Side Upload Progress
// Track upload progress
window.addEventListener('livecomponent:upload-progress', (e) => {
const { uploadId, progress, chunkIndex, totalChunks } = e.detail;
console.log(`Upload ${uploadId}: ${progress}%`);
console.log(`Chunk ${chunkIndex + 1} of ${totalChunks}`);
// Update UI
updateProgressBar(progress);
});
// Upload complete
window.addEventListener('livecomponent:upload-complete', (e) => {
const { uploadId, filename, fileSize } = e.detail;
showNotification(`Upload complete: ${filename}`);
});
// Upload error
window.addEventListener('livecomponent:upload-error', (e) => {
const { uploadId, error, chunkIndex } = e.detail;
console.error(`Upload failed at chunk ${chunkIndex}:`, error);
// Retry logic
if (e.detail.retryable) {
retryUpload(uploadId, chunkIndex);
}
});
Resumable Uploads
final class ResumableUploadHandler
{
public function resumeUpload(string $uploadId): UploadState
{
// Get upload state from storage
$state = $this->uploadStateRepository->find($uploadId);
if (!$state) {
throw new UploadNotFoundException($uploadId);
}
// Return chunks that still need uploading
$uploadedChunks = $this->getUploadedChunks($uploadId);
$remainingChunks = array_diff(
range(0, $state->totalChunks - 1),
$uploadedChunks
);
return new UploadState(
uploadId: $uploadId,
filename: $state->filename,
totalChunks: $state->totalChunks,
uploadedChunks: $uploadedChunks,
remainingChunks: $remainingChunks
);
}
}
Component Communication
Parent-Child Communication
// Parent component
final class Dashboard extends LiveComponent
{
#[LiveProp]
public array $childData = [];
#[LiveAction]
public function updateChild(array $data): void
{
$this->childData = $data;
// Broadcast update to child component
$this->emit('child-component-id', 'dataUpdated', $data);
}
}
// Child component
final class Widget extends LiveComponent
{
public function mount(): void
{
// Listen for parent events
$this->on('dataUpdated', fn($data) => $this->handleUpdate($data));
}
private function handleUpdate(array $data): void
{
$this->processData($data);
}
}
Sibling Communication
// Component A
#[LiveAction]
public function notifySibling(string $message): void
{
// Broadcast to specific component
$this->broadcast('component-b-id', 'messageReceived', [
'message' => $message,
'sender' => $this->id
]);
}
// Component B
public function mount(): void
{
$this->on('messageReceived', function($data) {
$this->handleMessage($data['message'], $data['sender']);
});
}
Global Events
// Broadcast to all components
final class GlobalNotification extends LiveComponent
{
#[LiveAction]
public function sendNotification(string $message): void
{
$this->broadcastGlobal('notification.new', [
'message' => $message,
'timestamp' => time()
]);
}
}
// Any component can listen
public function mount(): void
{
$this->onGlobal('notification.new', function($data) {
$this->notifications[] = $data;
});
}
State Management Patterns
Computed Properties
final class ShoppingCart extends LiveComponent
{
#[LiveProp]
public array $items = [];
// Computed property (not serialized)
public function getSubtotal(): float
{
return array_reduce(
$this->items,
fn($total, $item) => $total + ($item['price'] * $item['quantity']),
0.0
);
}
public function getTax(): float
{
return $this->getSubtotal() * 0.19;
}
public function getTotal(): float
{
return $this->getSubtotal() + $this->getTax();
}
}
Derived State
final class FilteredList extends LiveComponent
{
#[LiveProp]
public array $allItems = [];
#[LiveProp]
public string $searchTerm = '';
#[LiveProp]
public array $filters = [];
// Derived state
private array $filteredItems = [];
public function updated(string $property, mixed $oldValue, mixed $newValue): void
{
if (in_array($property, ['searchTerm', 'filters'])) {
$this->recalculateFilteredItems();
}
}
private function recalculateFilteredItems(): void
{
$this->filteredItems = array_filter(
$this->allItems,
fn($item) => $this->matchesFilters($item)
);
}
public function render(): string
{
return $this->view('filtered-list', [
'items' => $this->filteredItems
]);
}
}
State Persistence
use App\Framework\LiveComponents\Attributes\Persisted;
final class UserPreferences extends LiveComponent
{
#[LiveProp]
#[Persisted(storage: 'session', key: 'user.preferences')]
public array $preferences = [];
#[LiveAction]
public function updatePreference(string $key, mixed $value): void
{
$this->preferences[$key] = $value;
// Automatically persisted to session
}
}
Custom Actions
Action Middleware
use App\Framework\LiveComponents\Middleware\ActionMiddleware;
final class LoggingMiddleware implements ActionMiddleware
{
public function before(
LiveComponent $component,
string $action,
array $params
): void {
$this->logger->info("Action starting: {$action}", [
'component' => get_class($component),
'params' => $params
]);
}
public function after(
LiveComponent $component,
string $action,
mixed $result
): void {
$this->logger->info("Action completed: {$action}");
}
}
Custom Action Attributes
use App\Framework\LiveComponents\Attributes\CustomAction;
#[CustomAction(
name: 'tracked',
handler: TrackingActionHandler::class
)]
#[LiveAction]
public function importantAction(): void
{
// Action automatically tracked by TrackingActionHandler
}
Next: Best Practices →