Understanding how Diffyne manages component state.
All public properties are part of component state:
class Counter extends Component
{
public int $count = 0; // Part of state
public string $name = ''; // Part of state
protected int $secret = 42; // NOT part of state
private bool $flag = true; // NOT part of state
}Rule: Only public properties are reactive and synced with the client.
When component first renders:
public function mount()
{
$this->count = 0;
$this->items = [];
}State sent to browser:
{
"count": 0,
"items": []
}When user interacts:
Browser → Server: {method: 'increment', state: {count: 0}}
Server updates state: $this->count = 1
Server → Browser: {patches: [...], state: {count: 1}}
On each request, state is restored:
// Browser sends state
{
"count": 5,
"items": ["a", "b", "c"]
}
// Server hydrates component
$component->count = 5;
$component->items = ["a", "b", "c"];Use type hints for safety:
public string $name = '';
public int $age = 0;
public bool $active = false;
public array $items = [];
public ?User $user = null;Diffyne handles type-safe restoration, converting null values appropriately:
string→''int→0bool→falsearray→[]
Properties marked with #[Locked] cannot be updated from the client. Use for server-controlled data.
use Diffyne\Attributes\Locked;
class PostList extends Component
{
#[Locked]
public array $posts = []; // Cannot be changed from client
#[Locked]
public int $total = 0; // Server-calculated
public int $page = 1; // Can be changed from client
}- Server-controlled data: Database results, computed totals
- Security: Prevent tampering with sensitive data
- Derived values: Values calculated from other properties
class ShoppingCart extends Component
{
#[Locked]
public array $items = []; // Server loads from database
#[Locked]
public float $subtotal = 0.0; // Calculated server-side
public function addItem(int $productId): void
{
// Only server can modify items
$product = Product::find($productId);
$this->items[] = $product->toArray();
$this->recalculateTotals();
}
private function recalculateTotals(): void
{
$this->subtotal = array_sum(array_column($this->items, 'price'));
}
}Security Note: Attempting to update a locked property from the client will result in a 400 Bad Request error.
Properties marked with #[Computed] are excluded from state serialization. They're calculated, not stored.
use Diffyne\Attributes\Computed;
class TodoList extends Component
{
public array $todos = [];
#[Computed]
public int $completedCount = 0; // Not in state
public function getCompletedCount(): int
{
return count(array_filter($this->todos, fn($t) => $t['completed']));
}
public function getRemainingCount(): int
{
return count($this->todos) - $this->getCompletedCount();
}
}You can also use methods for computed values:
class TodoList extends Component
{
public array $todos = [];
public function getCompletedCount(): int
{
return count(array_filter($this->todos, fn($t) => $t['completed']));
}
public function getRemainingCount(): int
{
return count($this->todos) - $this->getCompletedCount();
}
}Use in views:
<p>{{ $this->getCompletedCount() }} completed</p>
<p>{{ $this->getRemainingCount() }} remaining</p>- Derived values: Values calculated from other properties
- Performance: Avoid storing redundant data
- Consistency: Always calculate from source of truth
Note: Computed properties are automatically hidden from state and cannot be updated from the client.
public function clearForm()
{
$this->reset();
}public function clearName()
{
$this->reset('name');
}
public function clearForm()
{
$this->reset('name', 'email', 'message');
}State exists only during request cycle. To persist across page loads:
public function dehydrate()
{
session()->put('component.data', $this->data);
}
public function hydrate()
{
$this->data = session()->get('component.data', []);
}public function mount()
{
$saved = UserPreference::where('user_id', auth()->id())
->where('key', 'filter_settings')
->first();
if ($saved) {
$this->filters = json_decode($saved->value, true);
}
}
public function saveFilters()
{
UserPreference::updateOrCreate(
['user_id' => auth()->id(), 'key' => 'filter_settings'],
['value' => json_encode($this->filters)]
);
}public array $user = [
'name' => '',
'email' => '',
'address' => [
'street' => '',
'city' => '',
'zip' => '',
],
];
public function updateAddress($field, $value)
{
$this->user['address'][$field] = $value;
}Convert to arrays for state:
public array $users = [];
public function mount()
{
$this->users = User::all()->toArray();
}// Good
public string $name = '';
public int $count = 0;
// Avoid
public $name;
public $count;// Good
public array $items = [];
// Avoid
public $items; // undefined// Avoid - models can't be serialized
public User $user;
// Good - store ID and reload
public int $userId;
public function hydrate()
{
$this->user = User::find($this->userId);
}// Good - computed method
public function getTotalPrice(): float
{
return array_sum(array_column($this->items, 'price'));
}
// Good - computed attribute
#[Computed]
public float $totalPrice = 0.0;
public function getTotalPrice(): float
{
return array_sum(array_column($this->items, 'price'));
}
// Avoid - storing computed value in state
public float $totalPrice; // Redundant, can get out of sync// Good - locked property
#[Locked]
public array $posts = [];
// Avoid - allows client tampering
public array $posts = [];- Attributes - Learn about Locked, Computed, and other attributes
- Security - Security features and best practices
- Lifecycle Hooks - Component lifecycle
- Virtual DOM - How state changes update DOM
- Performance - State optimization