State Serialization

When HTTP magics send requests, Gale serializes Alpine state into JSON. In v2.0, serialization only happens for state you explicitly opt in to send.

v2.0 Breaking Change
Gale sends no state by default. Use x-sync (component-wide) or include (per request) to opt in.

How Serialization Works

When you call an HTTP magic like $action('/save'), Gale collects only the state you opt in to send, then serializes it.

  1. Component x-data - Properties included via x-sync or include
  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.$action('/save') },
    validate() { return this.name.length > 0 }
}" x-sync>

    <!-- Request body: {"name":"John","email":"john@example.com","preferences":{"theme":"dark"}} -->
    <button @click="$action('/save')">Save</button>
</div>

Controlling State with x-sync

The x-sync directive gives you declarative control over which state properties get sent to the server. This is more convenient than using include options when you want the same synchronization behavior for all requests from a component.

Syntax Variants

Syntax Behavior Use Case
x-sync Send all serializable state (wildcard) Sync everything from this component
x-sync="*" Send all serializable state (explicit) Same as empty directive
x-sync="['a','b']" Send only specified properties Whitelist specific keys
x-sync="a, b" Send only specified properties (shorthand) Cleaner syntax for simple lists
No directive Send nothing by default Use include option per-request

Examples

Sync All State

<div x-data="{ name: '', email: '', phone: '' }" x-sync>
    <!-- All properties (name, email, phone) will be sent -->
    <button @click="$action('/save')">Save</button>
</div>

Sync Specific Properties (Array Syntax)

<div x-data="{
    name: '',
    email: '',
    password: '',        // Won't be sent
    confirmPassword: '', // Won't be sent
    _uiState: 'form'     // Won't be sent (underscore)
}" x-sync="['name', 'email']">
    <!-- Only name and email will be sent -->
    <button @click="$action('/register')">Register</button>
</div>

Sync Specific Properties (String Syntax)

<div x-data="{ title: '', description: '', status: '' }"
     x-sync="title, description">
    <!-- Only title and description will be sent (status excluded) -->
    <button @click="$action('/tasks')">Create Task</button>
</div>

Interaction with include/exclude

When using x-sync with include or exclude options, the option takes precedence for that specific request:

<div x-data="{ a: 1, b: 2, c: 3 }" x-sync="a, b">
    <!-- Normally sends {a: 1, b: 2} -->
    <button @click="$action('/save')">Save A & B</button>

    <!-- Override: sends {c: 3} only -->
    <button @click="$action('/save', { include: ['c'] })">Save C Only</button>
</div>
Best Practice
Use x-sync for component-wide synchronization behavior, and include/exclude options for request-specific overrides.

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="$action('/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' }" x-sync>
    <div x-data="{ postId: 456, postTitle: 'Hello' }" x-sync>

        <!-- Request includes state from BOTH parent and child -->
        <button @click="$action('/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
$action('/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

These patterns assume the component uses x-sync or the request uses include.

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
$action('/update-name', { include: ['name'] })

// Avoid: Sending entire state when only name is needed
$action('/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