State Serialization
When HTTP magics send requests, Gale automatically serializes your component's Alpine state into JSON. Understanding how serialization works helps you design your state structure and control what gets sent to the server.
How Serialization Works
When you call an HTTP magic like $postx('/save'), Gale collects state from:
- Component x-data - All properties defined in the component's Alpine data
- Parent scopes - State from parent components in nested structures
- Global stores - Alpine.store() data (if any)
- Named components - Other components when using
includeComponents
Then it filters and converts this state to JSON that's safe to send over HTTP.
Automatic Filtering
Certain properties are automatically excluded from serialization:
| Type | Example | Reason |
|---|---|---|
| Underscore prefix | _isLoading |
Browser-only state |
| Dollar prefix | $el, $refs |
Alpine internals |
| Functions | save(), validate() |
Cannot serialize functions |
| DOM Elements | this.$refs.input |
Cannot serialize DOM nodes |
<div x-data="{
// SENT to server
name: 'John',
email: 'john@example.com',
preferences: { theme: 'dark' },
// NOT sent (underscore = browser-only)
_isEditing: false,
_selectedTab: 'profile',
// NOT sent (functions excluded)
save() { this.$postx('/save') },
validate() { return this.name.length > 0 }
}">
<!-- Request body: {"name":"John","email":"john@example.com","preferences":{"theme":"dark"}} -->
<button @click="$postx('/save')">Save</button>
</div>
<div x-data="{
// SENT to server
name: 'John',
email: 'john@example.com',
preferences: { theme: 'dark' },
// NOT sent (underscore = browser-only)
_isEditing: false,
_selectedTab: 'profile',
// NOT sent (functions excluded)
save() { this.$postx('/save') },
validate() { return this.name.length > 0 }
}">
<!-- Request body: {"name":"John","email":"john@example.com","preferences":{"theme":"dark"}} -->
<button @click="$postx('/save')">Save</button>
</div>
Special Type Handling
Gale intelligently converts JavaScript types to JSON-compatible formats:
Dates
JavaScript Date objects are converted to ISO 8601 strings:
// JavaScript
{ createdAt: new Date('2024-01-15T10:30:00') }
// Serialized JSON
{ "createdAt": "2024-01-15T10:30:00.000Z" }
// JavaScript
{ createdAt: new Date('2024-01-15T10:30:00') }
// Serialized JSON
{ "createdAt": "2024-01-15T10:30:00.000Z" }
In Laravel, use Carbon to parse:
use Carbon\Carbon;
$createdAt = Carbon::parse($request->input('createdAt'));
use Carbon\Carbon;
$createdAt = Carbon::parse($request->input('createdAt'));
Maps and Sets
Map objects become plain objects, and Set objects become arrays:
// JavaScript Map
const settings = new Map([
['theme', 'dark'],
['language', 'en']
]);
// Serialized as: {"theme":"dark","language":"en"}
// JavaScript Set
const selectedIds = new Set([1, 2, 3]);
// Serialized as: [1,2,3]
// JavaScript Map
const settings = new Map([
['theme', 'dark'],
['language', 'en']
]);
// Serialized as: {"theme":"dark","language":"en"}
// JavaScript Set
const selectedIds = new Set([1, 2, 3]);
// Serialized as: [1,2,3]
Regular Expressions
RegExp objects are converted to their string representation:
// JavaScript
{ pattern: /^\d{3}-\d{4}$/ }
// Serialized JSON
{ "pattern": "/^\\d{3}-\\d{4}$/" }
// JavaScript
{ pattern: /^\d{3}-\d{4}$/ }
// Serialized JSON
{ "pattern": "/^\\d{3}-\\d{4}$/" }
Nested Objects and Arrays
Gale recursively serializes nested objects and arrays to any depth (up to safety limits):
<div x-data="{
user: {
profile: {
name: 'John',
address: {
street: '123 Main St',
city: 'Boston'
}
},
settings: {
notifications: true
}
},
items: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
]
}">
<!-- Full nested structure is serialized -->
<button @click="$postx('/save')">Save</button>
</div>
<div x-data="{
user: {
profile: {
name: 'John',
address: {
street: '123 Main St',
city: 'Boston'
}
},
settings: {
notifications: true
}
},
items: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
]
}">
<!-- Full nested structure is serialized -->
<button @click="$postx('/save')">Save</button>
</div>
Circular Reference Protection
Gale detects and handles circular references to prevent infinite loops:
// If your state has circular references:
const parent = { name: 'Parent' };
const child = { name: 'Child', parent: parent };
parent.child = child; // Circular!
// Serialized result:
{
"name": "Parent",
"child": {
"name": "Child",
"parent": "[Circular]" // Replaced with marker
}
}
// If your state has circular references:
const parent = { name: 'Parent' };
const child = { name: 'Child', parent: parent };
parent.child = child; // Circular!
// Serialized result:
{
"name": "Parent",
"child": {
"name": "Child",
"parent": "[Circular]" // Replaced with marker
}
}
Safety Limits
To prevent memory exhaustion and stack overflows, Gale enforces these limits:
| Limit | Value | When Exceeded |
|---|---|---|
| Max Depth | 50 levels | Value replaced with [MaxDepth] |
| Max Keys | 10,000 keys | Remaining keys replaced with [MaxKeys] |
| Max String Length | 100,000 chars | String truncated with ...[truncated] |
include option to send only
what's needed.
Handling Alpine Proxies
Alpine wraps your data in Proxy objects for reactivity. Gale automatically "collapses" these proxies to extract plain values:
// Alpine internally stores:
Proxy {
items: Proxy { 0: Proxy {...}, 1: Proxy {...} }
}
// Gale extracts plain values:
{
"items": [
{ "id": 1, "name": "Item 1" },
{ "id": 2, "name": "Item 2" }
]
}
// Alpine internally stores:
Proxy {
items: Proxy { 0: Proxy {...}, 1: Proxy {...} }
}
// Gale extracts plain values:
{
"items": [
{ "id": 1, "name": "Item 1" },
{ "id": 2, "name": "Item 2" }
]
}
Parent Scope Merging
When components are nested, Gale collects state from all parent scopes:
<div x-data="{ userId: 123, userName: 'John' }">
<div x-data="{ postId: 456, postTitle: 'Hello' }">
<!-- Request includes state from BOTH parent and child -->
<button @click="$postx('/save')">Save</button>
<!-- Serialized: {"userId":123,"userName":"John","postId":456,"postTitle":"Hello"} -->
</div>
</div>
<div x-data="{ userId: 123, userName: 'John' }">
<div x-data="{ postId: 456, postTitle: 'Hello' }">
<!-- Request includes state from BOTH parent and child -->
<button @click="$postx('/save')">Save</button>
<!-- Serialized: {"userId":123,"userName":"John","postId":456,"postTitle":"Hello"} -->
</div>
</div>
Component State Namespace
When using includeComponents, component state is placed under the
_components key to avoid conflicts:
// Request with includeComponents
$postx('/checkout', {
includeComponents: ['cart', 'shipping']
})
// Serialized structure:
{
// Local component state
"paymentMethod": "card",
// Named components under _components
"_components": {
"cart": {
"items": [...],
"total": 99.99
},
"shipping": {
"address": "123 Main St",
"method": "express"
}
}
}
// Request with includeComponents
$postx('/checkout', {
includeComponents: ['cart', 'shipping']
})
// Serialized structure:
{
// Local component state
"paymentMethod": "card",
// Named components under _components
"_components": {
"cart": {
"items": [...],
"total": 99.99
},
"shipping": {
"address": "123 Main St",
"method": "express"
}
}
}
Debugging Serialization
To see what gets serialized, you can log the request body in your controller:
public function debug(Request $request)
{
// Log all received state
Log::info('Received state:', $request->all());
// Or dump in development
dd($request->all());
return gale();
}
public function debug(Request $request)
{
// Log all received state
Log::info('Received state:', $request->all());
// Or dump in development
dd($request->all());
return gale();
}
You can also inspect the Network tab in browser DevTools to see the request payload.
Best Practices
1. Use Underscore Prefix for UI State
// Good: Clear separation of concerns
{
formData: { name: '', email: '' }, // Sent
_isSubmitting: false, // Not sent
_errors: {}, // Not sent
_showModal: false // Not sent
}
// Good: Clear separation of concerns
{
formData: { name: '', email: '' }, // Sent
_isSubmitting: false, // Not sent
_errors: {}, // Not sent
_showModal: false // Not sent
}
2. Use Include for Large Components
// Good: Only send what's needed
$postx('/update-name', { include: ['name'] })
// Avoid: Sending entire state when only name is needed
$postx('/update-name')
// Good: Only send what's needed
$postx('/update-name', { include: ['name'] })
// Avoid: Sending entire state when only name is needed
$postx('/update-name')
3. Flatten Deep Structures When Possible
// Better: Flat structure
{
name: 'John',
street: '123 Main St',
city: 'Boston'
}
// Deeply nested (harder to validate, more bytes)
{
user: {
profile: {
details: {
name: 'John',
address: {
street: '123 Main St',
city: 'Boston'
}
}
}
}
}
// Better: Flat structure
{
name: 'John',
street: '123 Main St',
city: 'Boston'
}
// Deeply nested (harder to validate, more bytes)
{
user: {
profile: {
details: {
name: 'John',
address: {
street: '123 Main St',
city: 'Boston'
}
}
}
}
}