# LiveComponents End-to-End Guide **Complete guide to building LiveComponents from scratch to production.** This guide walks you through creating, testing, and deploying LiveComponents with all features. --- ## Table of Contents 1. [Quick Start (5 Minutes)](#quick-start-5-minutes) 2. [Component Basics](#component-basics) 3. [State Management](#state-management) 4. [Actions & Events](#actions--events) 5. [Advanced Features](#advanced-features) 6. [Performance Optimization](#performance-optimization) 7. [Real-World Examples](#real-world-examples) --- ## Quick Start (5 Minutes) ### Step 1: Create Component Class ```php $this->id->toString(), 'count' => $this->state->count, ] ); } #[Action] public function increment(): CounterState { return $this->state->increment(); } } ``` ### Step 2: Create State Value Object ```php count + 1); } public static function empty(): self { return new self(); } public static function fromArray(array $data): self { return new self( count: $data['count'] ?? 0 ); } public function toArray(): array { return [ 'count' => $this->count, ]; } } ``` ### Step 3: Create Template **File**: `resources/templates/livecomponent-counter.view.php` ```html

Counter: {count}

``` ### Step 4: Use in Template ```html ``` **That's it!** Your component is ready to use. --- ## Component Basics ### Component Structure Every LiveComponent consists of: 1. **Component Class** - Implements `LiveComponentContract` 2. **State Value Object** - Extends `ComponentState` 3. **Template** - HTML template with placeholders 4. **Actions** - Methods marked with `#[Action]` ### Component ID Components are identified by a `ComponentId` consisting of: - **Name**: Component name from `#[LiveComponent]` attribute - **Instance ID**: Unique identifier (e.g., 'demo', 'user-123') ```php ComponentId::create('counter', 'demo') // counter:demo ComponentId::create('user-profile', '123') // user-profile:123 ``` ### RenderData `getRenderData()` returns template path and data: ```php public function getRenderData(): ComponentRenderData { return new ComponentRenderData( templatePath: 'livecomponent-counter', data: [ 'componentId' => $this->id->toString(), 'count' => $this->state->count, 'lastUpdate' => $this->state->lastUpdate, ] ); } ``` --- ## State Management ### State Value Objects State is managed through immutable Value Objects: ```php final readonly class CounterState extends ComponentState { public function __construct( public int $count = 0, public ?string $lastUpdate = null ) { } // Factory methods public static function empty(): self { return new self(); } public static function fromArray(array $data): self { return new self( count: $data['count'] ?? 0, lastUpdate: $data['lastUpdate'] ?? null ); } // Transformation methods (return new instance) public function increment(): self { return new self( count: $this->count + 1, lastUpdate: date('H:i:s') ); } public function reset(): self { return new self( count: 0, lastUpdate: date('H:i:s') ); } // Serialization public function toArray(): array { return [ 'count' => $this->count, 'lastUpdate' => $this->lastUpdate, ]; } } ``` ### State Updates Actions return new State instances: ```php #[Action] public function increment(): CounterState { return $this->state->increment(); } #[Action] public function addAmount(int $amount): CounterState { return $this->state->addAmount($amount); } ``` --- ## Actions & Events ### Actions Actions are public methods marked with `#[Action]`: ```php #[Action] public function increment(): CounterState { return $this->state->increment(); } #[Action(rateLimit: 10, idempotencyTTL: 60)] public function expensiveOperation(): CounterState { // Rate limited to 10 calls per minute // Idempotent for 60 seconds return $this->state->performExpensiveOperation(); } ``` ### Action Parameters Actions can accept parameters: ```php #[Action] public function addAmount(int $amount): CounterState { return $this->state->addAmount($amount); } ``` **Client-side**: ```html ``` ### Events Dispatch events from actions: ```php #[Action] public function increment(?ComponentEventDispatcher $events = null): CounterState { $newState = $this->state->increment(); $events?->dispatch('counter:changed', EventPayload::fromArray([ 'old_value' => $this->state->count, 'new_value' => $newState->count, ])); return $newState; } ``` **Client-side**: ```javascript document.addEventListener('livecomponent:event', (e) => { if (e.detail.name === 'counter:changed') { console.log('Counter changed:', e.detail.data); } }); ``` --- ## Advanced Features ### Caching Implement `Cacheable` interface: ```php #[LiveComponent('user-stats')] final readonly class UserStatsComponent implements LiveComponentContract, Cacheable { public function getCacheKey(): string { return 'user-stats:' . $this->state->userId; } public function getCacheTTL(): Duration { return Duration::fromMinutes(5); } public function shouldCache(): bool { return true; } public function getCacheTags(): array { return ['user-stats', 'user:' . $this->state->userId]; } public function getVaryBy(): ?VaryBy { return VaryBy::userId(); } public function getStaleWhileRevalidate(): ?Duration { return Duration::fromHours(1); } } ``` ### Polling Implement `Pollable` interface: ```php #[LiveComponent('live-feed')] final readonly class LiveFeedComponent implements LiveComponentContract, Pollable { public function poll(): LiveComponentState { // Fetch latest data $newItems = $this->feedService->getLatest(); return $this->state->withItems($newItems); } public function getPollInterval(): int { return 5000; // Poll every 5 seconds } } ``` ### File Uploads Implement `SupportsFileUpload` interface: ```php #[LiveComponent('image-uploader')] final readonly class ImageUploaderComponent implements LiveComponentContract, SupportsFileUpload { public function handleUpload( UploadedFile $file, ?ComponentEventDispatcher $events = null ): LiveComponentState { // Process upload $path = $this->storage->store($file); $events?->dispatch('file:uploaded', EventPayload::fromArray([ 'path' => $path, 'size' => $file->getSize(), ])); return $this->state->withUploadedFile($path); } public function validateUpload(UploadedFile $file): array { $errors = []; if (!in_array($file->getMimeType(), ['image/jpeg', 'image/png'])) { $errors[] = 'Only JPEG and PNG images are allowed'; } if ($file->getSize() > 5 * 1024 * 1024) { $errors[] = 'File size must be less than 5MB'; } return $errors; } public function getAllowedMimeTypes(): array { return ['image/jpeg', 'image/png']; } public function getMaxFileSize(): int { return 5 * 1024 * 1024; // 5MB } } ``` ### Fragments Use fragments for partial updates: **Template**: ```html

Count: {count}

``` **Client-side**: Only `counter-display` fragment is updated, not the entire component. ### Slots Implement `SupportsSlots` for flexible component composition: ```php #[LiveComponent('card')] final readonly class CardComponent implements LiveComponentContract, SupportsSlots { public function getSlotDefinitions(): array { return [ SlotDefinition::named('header'), SlotDefinition::default('

Default content

'), SlotDefinition::named('footer'), ]; } public function getSlotContext(string $slotName): SlotContext { return SlotContext::create([ 'card_title' => $this->state->title, ]); } public function processSlotContent(SlotContent $content): SlotContent { return $content; // No processing needed } public function validateSlots(array $providedSlots): array { return []; // No validation errors } } ``` **Usage**: ```html

Custom Header

Main content

``` ### Islands Use `#[Island]` for isolated rendering: ```php #[LiveComponent('heavy-widget')] #[Island(isolated: true, lazy: true, placeholder: 'Loading widget...')] final readonly class HeavyWidgetComponent implements LiveComponentContract { // Component implementation } ``` --- ## Performance Optimization ### Caching Strategies **Global Cache**: ```php public function getVaryBy(): ?VaryBy { return VaryBy::none(); // Same for all users } ``` **Per-User Cache**: ```php public function getVaryBy(): ?VaryBy { return VaryBy::userId(); // Different cache per user } ``` **Per-User Per-Locale Cache**: ```php public function getVaryBy(): ?VaryBy { return VaryBy::userAndLocale(); // Different cache per user and locale } ``` **With Feature Flags**: ```php public function getVaryBy(): ?VaryBy { return VaryBy::userId() ->withFeatureFlags(['new-ui', 'dark-mode']); } ``` ### Stale-While-Revalidate Serve stale content while refreshing: ```php public function getCacheTTL(): Duration { return Duration::fromMinutes(5); // Fresh for 5 minutes } public function getStaleWhileRevalidate(): ?Duration { return Duration::fromHours(1); // Serve stale for 1 hour while refreshing } ``` ### Request Batching Batch multiple operations: ```javascript // Client-side const response = await LiveComponent.executeBatch([ { componentId: 'counter:demo', method: 'increment', params: { amount: 5 }, fragments: ['counter-display'] }, { componentId: 'stats:user-123', method: 'refresh' } ]); ``` ### Lazy Loading Load components when entering viewport: ```php // In template {{ lazy_component('notification-center:user-123', [ 'priority' => 'high', 'threshold' => '0.1', 'placeholder' => 'Loading notifications...' ]) }} ``` --- ## Real-World Examples ### Example 1: User Profile Component ```php #[LiveComponent('user-profile')] #[Island(isolated: true, lazy: true)] final readonly class UserProfileComponent implements LiveComponentContract, Cacheable { public function __construct( public ComponentId $id, public UserProfileState $state ) { } public function getRenderData(): ComponentRenderData { return new ComponentRenderData( templatePath: 'livecomponent-user-profile', data: [ 'componentId' => $this->id->toString(), 'user' => $this->state->user, 'stats' => $this->state->stats, ] ); } #[Action] public function updateProfile(array $data, ?ComponentEventDispatcher $events = null): UserProfileState { $updatedUser = $this->userService->update($this->state->user->id, $data); $events?->dispatch('profile:updated', EventPayload::fromArray([ 'user_id' => $updatedUser->id, ])); return $this->state->withUser($updatedUser); } // Cacheable implementation public function getCacheKey(): string { return 'user-profile:' . $this->state->user->id; } public function getCacheTTL(): Duration { return Duration::fromMinutes(10); } public function shouldCache(): bool { return true; } public function getCacheTags(): array { return ['user-profile', 'user:' . $this->state->user->id]; } public function getVaryBy(): ?VaryBy { return VaryBy::userId(); } public function getStaleWhileRevalidate(): ?Duration { return null; // No SWR for user profiles } } ``` ### Example 2: Search Component with Debouncing ```php #[LiveComponent('search')] final readonly class SearchComponent implements LiveComponentContract { #[Action(rateLimit: 20)] // Allow 20 searches per minute public function search(string $query, ?ComponentEventDispatcher $events = null): SearchState { if (strlen($query) < 3) { return $this->state->withResults([]); } $results = $this->searchService->search($query); $events?->dispatch('search:completed', EventPayload::fromArray([ 'query' => $query, 'result_count' => count($results), ])); return $this->state->withQuery($query)->withResults($results); } } ``` **Client-side debouncing**: ```javascript let searchTimeout; const searchInput = document.querySelector('[data-live-action="search"]'); searchInput.addEventListener('input', (e) => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { LiveComponent.executeAction('search:demo', 'search', { query: e.target.value }); }, 300); // 300ms debounce }); ``` ### Example 3: Dashboard with Multiple Components ```php // Dashboard page template
``` --- ## Testing ### Using LiveComponentTestCase ```php class CounterComponentTest extends LiveComponentTestCase { public function test_increments_counter(): void { $this->mount('counter:test', ['count' => 5]) ->call('increment') ->seeStateKey('count', 6) ->seeHtmlHas('Count: 6'); } public function test_resets_counter(): void { $this->mount('counter:test', ['count' => 10]) ->call('reset') ->seeStateKey('count', 0) ->seeHtmlHas('Count: 0'); } public function test_dispatches_event_on_increment(): void { $this->mount('counter:test', ['count' => 5]) ->call('increment') ->seeEventDispatched('counter:changed', [ 'old_value' => 5, 'new_value' => 6, ]); } } ``` ### Snapshot Testing ```php public function test_renders_counter_correctly(): void { $component = mountComponent('counter:test', ['count' => 5]); expect($component['html'])->toMatchSnapshot('counter-initial-state'); } ``` --- ## Best Practices ### 1. Immutable State Always return new State instances: ```php // ✅ CORRECT public function increment(): CounterState { return new CounterState(count: $this->state->count + 1); } // ❌ WRONG (won't work with readonly) public function increment(): void { $this->state->count++; // Error: Cannot modify readonly property } ``` ### 2. Type-Safe State Use Value Objects instead of arrays: ```php // ✅ CORRECT public function __construct( public CounterState $state ) {} // ❌ WRONG public function __construct( public array $state ) {} ``` ### 3. Action Validation Validate inputs in actions: ```php #[Action] public function addAmount(int $amount): CounterState { if ($amount < 0) { throw new \InvalidArgumentException('Amount must be positive'); } return $this->state->addAmount($amount); } ``` ### 4. Error Handling Handle errors gracefully: ```php #[Action] public function performOperation(): CounterState { try { return $this->state->performOperation(); } catch (\Exception $e) { // Log error error_log("Operation failed: " . $e->getMessage()); // Return unchanged state or error state return $this->state->withError($e->getMessage()); } } ``` ### 5. Performance - Use caching for expensive operations - Use fragments for partial updates - Use lazy loading for below-the-fold content - Use Islands for heavy components - Batch multiple operations --- ## Next Steps - [Security Guide](security-guide-complete.md) - CSRF, Rate Limiting, Authorization - [Performance Guide](performance-guide-complete.md) - Caching, Batching, Optimization - [Upload Guide](upload-guide-complete.md) - File Uploads, Validation, Chunking - [API Reference](api-reference-complete.md) - Complete API documentation