# LiveComponents End-to-End Creation Guide
Complete guide for creating LiveComponents from scratch - from concept to production deployment.
## Table of Contents
- [Overview](#overview)
- [Quick Start](#quick-start)
- [Step-by-Step Tutorial](#step-by-step-tutorial)
- [Component Architecture](#component-architecture)
- [Advanced Patterns](#advanced-patterns)
- [Testing](#testing)
- [Deployment](#deployment)
- [Troubleshooting](#troubleshooting)
---
## Overview
A **LiveComponent** is a server-rendered, interactive UI component with automatic state synchronization. Components are:
- **Server-rendered**: HTML generated on the server
- **Stateful**: Maintain state across interactions
- **Interactive**: Respond to user actions without full page reloads
- **Framework-compliant**: Readonly classes, Value Objects, Composition
### When to Use LiveComponents
✅ **Use LiveComponents for:**
- Interactive forms with validation
- Real-time dashboards
- Shopping carts
- Search interfaces with filters
- File uploads with progress
- Paginated lists
- Wizards/multi-step forms
❌ **Don't use LiveComponents for:**
- Static content (use regular templates)
- Simple links (use standard HTML)
- High-frequency updates (use WebSockets directly)
- Complex client-side logic (use dedicated JavaScript)
---
## Quick Start
### Minimal Component (5 minutes)
```php
state->increment();
}
#[Action]
public function decrement(): CounterState
{
return $this->state->decrement();
}
// 5. Render method returns template path
public function render(): string
{
return 'components/counter';
}
}
```
### State Value Object
```php
count + 1,
lastUpdate: new DateTimeImmutable()
);
}
public function decrement(): self
{
return new self(
count: max(0, $this->count - 1), // Prevent negative
lastUpdate: new DateTimeImmutable()
);
}
// Required by ComponentData
public function toArray(): array
{
return [
'count' => $this->count,
'lastUpdate' => $this->lastUpdate?->format('Y-m-d H:i:s')
];
}
}
```
### Template
```html
Counter: {count}
Last update: {lastUpdate}
```
### Usage in Controller
```php
use App\Framework\LiveComponents\ComponentRegistry;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\View\ViewResult;
public function show(ComponentRegistry $registry): ViewResult
{
$component = $registry->resolve(
ComponentId::fromString('counter:main'),
initialData: ['count' => 0]
);
return new ViewResult('pages/counter', [
'component' => $component
]);
}
```
**That's it!** Component is now interactive. Actions automatically sync state to the server.
---
## Step-by-Step Tutorial
Let's build a **Shopping Cart Component** from scratch.
### Step 1: Define the Domain
```php
price->multiply($this->quantity);
}
public function withQuantity(int $quantity): self
{
return new self(
$this->productId,
$this->name,
$this->price,
$quantity
);
}
public function toArray(): array
{
return [
'product_id' => $this->productId,
'name' => $this->name,
'price' => $this->price->toDecimal(),
'quantity' => $this->quantity,
'total' => $this->getTotal()->toDecimal()
];
}
}
```
### Step 2: Create State Value Object
```php
$items */
public function __construct(
public array $items = [],
public ?string $couponCode = null,
public int $discountPercentage = 0
) {}
public function addItem(CartItem $item): self
{
// Check if item already exists
foreach ($this->items as $existingItem) {
if ($existingItem->productId === $item->productId) {
// Update quantity
return $this->updateQuantity(
$item->productId,
$existingItem->quantity + $item->quantity
);
}
}
// Add new item
return new self(
items: [...$this->items, $item],
couponCode: $this->couponCode,
discountPercentage: $this->discountPercentage
);
}
public function removeItem(string $productId): self
{
return new self(
items: array_values(array_filter(
$this->items,
fn(CartItem $item) => $item->productId !== $productId
)),
couponCode: $this->couponCode,
discountPercentage: $this->discountPercentage
);
}
public function updateQuantity(string $productId, int $quantity): self
{
if ($quantity < 1) {
return $this->removeItem($productId);
}
$updatedItems = array_map(
fn(CartItem $item) => $item->productId === $productId
? $item->withQuantity($quantity)
: $item,
$this->items
);
return new self(
items: $updatedItems,
couponCode: $this->couponCode,
discountPercentage: $this->discountPercentage
);
}
public function applyCoupon(string $couponCode, int $discountPercentage): self
{
return new self(
items: $this->items,
couponCode: $couponCode,
discountPercentage: $discountPercentage
);
}
public function getSubtotal(): Price
{
$total = 0;
foreach ($this->items as $item) {
$total += $item->getTotal()->cents;
}
return Price::fromCents($total);
}
public function getDiscount(): Price
{
if ($this->discountPercentage === 0) {
return Price::fromCents(0);
}
$subtotal = $this->getSubtotal();
$discountAmount = ($subtotal->cents * $this->discountPercentage) / 100;
return Price::fromCents((int) $discountAmount);
}
public function getTotal(): Price
{
return $this->getSubtotal()->subtract($this->getDiscount());
}
public function getItemCount(): int
{
return array_reduce(
$this->items,
fn(int $sum, CartItem $item) => $sum + $item->quantity,
0
);
}
public function isEmpty(): bool
{
return empty($this->items);
}
public function toArray(): array
{
return [
'items' => array_map(fn(CartItem $item) => $item->toArray(), $this->items),
'coupon_code' => $this->couponCode,
'discount_percentage' => $this->discountPercentage,
'subtotal' => $this->getSubtotal()->toDecimal(),
'discount' => $this->getDiscount()->toDecimal(),
'total' => $this->getTotal()->toDecimal(),
'item_count' => $this->getItemCount(),
'is_empty' => $this->isEmpty()
];
}
}
```
### Step 3: Create Component
```php
state->addItem($item);
// Dispatch event for analytics
$events?->dispatch('cart:item-added', EventPayload::fromArray([
'product_id' => $product_id,
'quantity' => $quantity,
'total_items' => $newState->getItemCount()
]));
return $newState;
}
#[Action]
public function removeItem(
string $product_id,
?ComponentEventDispatcher $events = null
): ShoppingCartState {
$newState = $this->state->removeItem($product_id);
$events?->dispatch('cart:item-removed', EventPayload::fromArray([
'product_id' => $product_id,
'total_items' => $newState->getItemCount()
]));
return $newState;
}
#[Action]
public function updateQuantity(
string $product_id,
int $quantity
): ShoppingCartState {
return $this->state->updateQuantity($product_id, $quantity);
}
#[Action(rateLimit: 5, rateLimitWindow: 300)] // 5 coupon attempts per 5 min
public function applyCoupon(
string $coupon_code,
?ComponentEventDispatcher $events = null
): ShoppingCartState {
// Validate coupon via service
$coupon = $this->couponService->validate($coupon_code);
if (!$coupon->isValid()) {
$events?->dispatch('cart:coupon-invalid', EventPayload::fromArray([
'coupon_code' => $coupon_code,
'reason' => $coupon->getInvalidReason()
]));
// Return current state unchanged
return $this->state;
}
$newState = $this->state->applyCoupon(
$coupon_code,
$coupon->getDiscountPercentage()
);
$events?->dispatch('cart:coupon-applied', EventPayload::fromArray([
'coupon_code' => $coupon_code,
'discount_percentage' => $coupon->getDiscountPercentage(),
'discount_amount' => $newState->getDiscount()->toDecimal()
]));
return $newState;
}
#[Action(
rateLimit: 3,
rateLimitWindow: 300,
idempotencyTTL: 3600
)]
public function checkout(?ComponentEventDispatcher $events = null): ShoppingCartState
{
if ($this->state->isEmpty()) {
$events?->dispatch('cart:checkout-failed', EventPayload::fromArray([
'reason' => 'empty_cart'
]));
return $this->state;
}
// Process checkout (would integrate with payment service)
$events?->dispatch('cart:checkout-initiated', EventPayload::fromArray([
'total' => $this->state->getTotal()->toDecimal(),
'item_count' => $this->state->getItemCount()
]));
return $this->state;
}
public function render(): string
{
return 'components/shopping-cart';
}
}
```
### Step 4: Create Template
```html
```
### Step 5: JavaScript Integration (Optional)
```javascript
// resources/js/modules/shopping-cart.js
import { LiveComponent } from './live-component.js';
const cartComponent = new LiveComponent('shopping-cart:main');
// Listen to events
cartComponent.on('cart:item-added', (data) => {
console.log('Item added:', data.product_id);
// Show notification
showNotification(`Added ${data.quantity} item(s) to cart`);
// Update cart badge
updateCartBadge(data.total_items);
});
cartComponent.on('cart:coupon-applied', (data) => {
showNotification(
`Coupon applied! You save ${data.discount_percentage}%`,
'success'
);
});
cartComponent.on('cart:coupon-invalid', (data) => {
showNotification(
`Invalid coupon: ${data.reason}`,
'error'
);
});
cartComponent.on('cart:checkout-initiated', (data) => {
// Redirect to checkout page
window.location.href = `/checkout?total=${data.total}`;
});
// Fragment updates for better performance
cartComponent.on('action:updateQuantity', async (event) => {
// Only update cart-items and cart-summary fragments
await cartComponent.executeAction(
'updateQuantity',
event.params,
{ fragments: ['cart-items', 'cart-summary'] }
);
});
```
### Step 6: Testing
```php
toHaveStateKey('is_empty', true);
expect($component['state'])->toHaveStateKey('item_count', 0);
});
it('adds items to cart', function () {
$component = mountComponent('shopping-cart:test');
$result = callAction($component, 'addItem', [
'product_id' => 'product-123',
'name' => 'Test Product',
'price_cents' => 1999,
'quantity' => 2
]);
expect($result['state'])->toHaveStateKey('item_count', 2);
expect($result['state'])->toHaveStateKey('subtotal', '39.98');
expect($result['events'])->toHaveDispatchedEvent('cart:item-added');
});
it('applies valid coupon', function () {
$component = mountComponent('shopping-cart:test');
// Add item first
$component = callAction($component, 'addItem', [
'product_id' => 'product-123',
'name' => 'Test Product',
'price_cents' => 10000,
'quantity' => 1
]);
// Apply coupon
$result = callAction($component, 'applyCoupon', [
'coupon_code' => 'SAVE20'
]);
expect($result['state'])->toHaveStateKey('discount_percentage', 20);
expect($result['state'])->toHaveStateKey('discount', '20.00');
expect($result['state'])->toHaveStateKey('total', '80.00');
expect($result['events'])->toHaveDispatchedEvent('cart:coupon-applied');
});
it('enforces rate limiting on checkout', function () {
$component = mountComponent('shopping-cart:test');
// Add item
$component = callAction($component, 'addItem', [
'product_id' => 'product-123',
'name' => 'Test Product',
'price_cents' => 10000
]);
// Try to checkout 4 times (limit is 3)
callAction($component, 'checkout');
callAction($component, 'checkout');
callAction($component, 'checkout');
// 4th attempt should be rate limited
expect(fn() => callAction($component, 'checkout'))
->toThrow(RateLimitExceededException::class);
});
});
```
---
## Component Architecture
### Required Elements
Every LiveComponent must have:
1. **#[LiveComponent] attribute** with unique name
2. **LiveComponentContract implementation**
3. **ComponentId + State in constructor**
4. **State as ComponentData Value Object**
5. **render() method returning template path**
### Optional Elements
- **#[Action] methods** for interactivity
- **Service injection** via constructor
- **Event dispatching** for side effects
- **Pollable interface** for auto-refresh
- **Fragment markers** for partial updates
### Component Lifecycle
```
1. Resolve (ComponentRegistry)
├─ Create instance via DI Container
├─ Inject ComponentId
├─ Inject initial State
└─ Inject Services
2. Render (LiveComponentRenderer)
├─ Call render() for template path
├─ Pass state data to template
├─ Process template (placeholders, conditionals, loops)
└─ Return HTML
3. Action Execution (LiveComponentHandler)
├─ Validate CSRF token
├─ Check rate limits
├─ Check idempotency
├─ Bind parameters
├─ Execute action method
├─ Create new State from return value
├─ Dispatch events
└─ Return ComponentUpdate
4. Re-render
├─ Create new component instance with new State
├─ Render updated HTML
├─ Extract fragments (if requested)
└─ Return response (html/fragments + state + events)
```
---
## Advanced Patterns
### Service Injection
```php
#[LiveComponent('user-profile')]
final readonly class UserProfileComponent implements LiveComponentContract
{
public function __construct(
public ComponentId $id,
public UserProfileState $state,
// Inject services via constructor
private UserRepository $userRepository,
private AvatarService $avatarService,
private PermissionChecker $permissions
) {}
#[Action]
public function updateAvatar(UploadedFile $avatar): UserProfileState
{
$url = $this->avatarService->upload($avatar);
return $this->state->withAvatarUrl($url);
}
}
```
### Event Dispatching
```php
#[Action]
public function completeOrder(
?ComponentEventDispatcher $events = null
): OrderState {
$order = $this->orderService->complete($this->state->orderId);
// Dispatch domain event
$events?->dispatch('order:completed', EventPayload::fromArray([
'order_id' => $order->id,
'total' => $order->total->toDecimal(),
'customer_id' => $order->customerId
]));
// Other systems can listen to this event
// - Send confirmation email
// - Update inventory
// - Notify warehouse
// - Track analytics
return $this->state->withOrder($order);
}
```
### Polling for Real-Time Updates
```php
use App\Framework\LiveComponents\Contracts\Pollable;
#[LiveComponent('order-status')]
final readonly class OrderStatusComponent implements
LiveComponentContract,
Pollable
{
#[Action]
public function poll(): OrderStatusState
{
// Fetch latest order status
$order = $this->orderRepository->find($this->state->orderId);
return $this->state->withStatus($order->status);
}
public function getPollInterval(): int
{
return 5000; // Poll every 5 seconds
}
}
```
### Fragment-Based Updates
```php
// Template with fragments
// JavaScript
liveComponent.executeAction('changePage', { page: 2 }, {
fragments: ['product-grid', 'pagination'] // Only update these
});
```
### DTOs for Complex Parameters
```php
// Define DTO
final readonly class UpdateProfileRequest
{
public function __construct(
public string $name,
public Email $email,
public ?string $bio = null,
public bool $newsletter = false
) {}
}
// Use in action
#[Action]
public function updateProfile(UpdateProfileRequest $request): UserProfileState
{
// $request is automatically instantiated from action parameters
return $this->state->update(
name: $request->name,
email: $request->email,
bio: $request->bio,
newsletter: $request->newsletter
);
}
```
---
## Testing
See `tests/Feature/LiveComponents/CounterComponentTest.php` for examples.
### Testing Helpers
```php
use function Pest\LiveComponents\mountComponent;
use function Pest\LiveComponents\callAction;
use function Pest\LiveComponents\callActionWithFragments;
// Mount component
$component = mountComponent('counter:test', ['count' => 0]);
// Execute action
$result = callAction($component, 'increment');
// Execute action with fragments
$result = callActionWithFragments(
$component,
'updateGrid',
params: ['page' => 2],
fragments: ['product-grid', 'pagination']
);
```
### Custom Expectations
```php
// HTML assertions
expect($component['html'])->toContainHtml('Count: 5');
// State assertions
expect($component['state'])->toHaveState(['count' => 5]);
expect($component['state'])->toHaveStateKey('count', 5);
// Event assertions
expect($result['events'])->toHaveDispatchedEvent('counter:changed');
expect($result['events'])->toHaveDispatchedEventWithData('counter:changed', [
'old_value' => 4,
'new_value' => 5
]);
// Fragment assertions
expect($result['html'])->toHaveFragment('cart-summary', '€99.99');
```
---
## Deployment
### Production Checklist
- [ ] All actions have `#[Action]` attribute
- [ ] Rate limits configured for critical actions
- [ ] Idempotency TTL set for state-changing actions
- [ ] CSRF protection enabled (default)
- [ ] Events logged for analytics
- [ ] Error handling implemented
- [ ] Tests passing
- [ ] Templates optimized (fragments for large updates)
- [ ] JavaScript client configured
- [ ] Monitoring/alerting set up
### Performance Tips
1. **Use fragments** for partial updates
2. **Configure polling interval** based on data freshness needs
3. **Add caching** via `#[Action(cache: true)]`
4. **Optimize templates** - minimize DOM size
5. **Lazy load** components not immediately visible
### Security Tips
1. **Never trust client input** - validate in actions
2. **Use DTOs** for complex parameters
3. **Set rate limits** on all actions
4. **Enable idempotency** for critical operations
5. **Log security events** for monitoring
---
## Troubleshooting
### Component Not Found
**Error**: "Component 'my-component' not found"
**Causes**:
- Missing `#[LiveComponent]` attribute
- Typo in component name
- Component class not in `Application/LiveComponents/` directory
- Discovery cache stale
**Solutions**:
```bash
# Clear discovery cache
php console.php discovery:clear
# Verify component is registered
php console.php mcp:server
# Then use analyze_codebase to find LiveComponents
```
### Action Not Callable
**Error**: "Action 'myAction' not found"
**Causes**:
- Missing `#[Action]` attribute
- Method is private/protected
- Method is static
- Reserved method name (render, toArray, etc.)
**Solutions**:
```php
// ✅ Correct
#[Action]
public function myAction(): StateType
// ❌ Wrong - missing attribute
public function myAction(): StateType
// ❌ Wrong - static
#[Action]
public static function myAction(): StateType
// ❌ Wrong - private
#[Action]
private function myAction(): StateType
```
### State Not Updating
**Causes**:
- Action not returning new state
- State mutation instead of new instance
- Caching issues
**Solutions**:
```php
// ❌ Wrong - mutation (doesn't work with readonly)
public function increment(): void
{
$this->state->count++; // Can't mutate readonly
}
// ✅ Correct - return new state
public function increment(): CounterState
{
return $this->state->increment(); // Returns new instance
}
```
### Events Not Dispatching
**Causes**:
- Missing `ComponentEventDispatcher` parameter
- Not calling `dispatch()` method
- Events not handled in JavaScript
**Solutions**:
```php
// ✅ Correct
#[Action]
public function save(?ComponentEventDispatcher $events = null): StateType
{
$events?->dispatch('saved', EventPayload::fromArray(['id' => 123]));
return $newState;
}
// JavaScript
liveComponent.on('saved', (data) => {
console.log('Saved:', data.id);
});
```
---
## Next Steps
- Read [Security Guide](SECURITY-GUIDE.md) for CSRF/Rate Limiting/Idempotency
- Read [Performance Guide](PERFORMANCE-GUIDE.md) for optimization strategies
- Read [API Reference](API-REFERENCE.md) for complete attribute/contract documentation
- Explore example components in `src/Application/LiveComponents/`
- Run tests: `./vendor/bin/pest tests/Feature/LiveComponents/`
---
## Summary
LiveComponents provide:
✅ **Server-rendered interactivity** without JavaScript complexity
✅ **Type-safe state management** with Value Objects
✅ **Built-in security** (CSRF, Rate Limiting, Idempotency)
✅ **Framework-compliant** (Readonly, Immutable, Composition)
✅ **Easy testing** with Pest helpers
✅ **Production-ready** with monitoring and error handling
**Start simple** with a counter, then build more complex components as you learn the patterns.