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:

  1. Component x-data - All properties defined in the component's Alpine data
  2. Parent scopes - State from parent components in nested structures
  3. Global stores - Alpine.store() data (if any)
  4. 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>

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" }

In Laravel, use Carbon to parse:

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]

Regular Expressions

RegExp objects are converted to their string representation:

// 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>

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
    }
}

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]
Tip: These limits are generous for normal use. If you're hitting them, consider restructuring your state or using the 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" }
    ]
}

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>

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"
        }
    }
}

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();
}

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
}

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')

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'
                }
            }
        }
    }
}

On this page