# 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