Files
michaelschiemer/docs/livecomponents/best-practices.md
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- 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.
2025-10-25 19:18:37 +02:00

23 KiB

LiveComponents Best Practices

Production-ready patterns and best practices for building scalable, maintainable LiveComponents.

Table of Contents


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

  1. Keep components focused - Single responsibility
  2. Minimize state - Compute derived values
  3. Use fragments - For large components (>5KB)
  4. Debounce expensive actions - Search, API calls
  5. Cache when possible - Expensive queries
  6. Rate limit everything - Public endpoints
  7. Authorize sensitive actions - Admin operations
  8. Validate all input - Client + server
  9. Encrypt sensitive data - PII, financial
  10. Test thoroughly - Unit, integration, E2E
  11. Handle errors gracefully - User-friendly messages
  12. Make it accessible - WCAG 2.1 AA
  13. Monitor production - Performance + errors
  14. Use optimistic updates - Instant feedback
  15. Document your code - Future you will thank you

For more details, see: