State Management

Bidirectional state synchronization between Laravel and Alpine.js.

How State Flows

Gale synchronizes state between your frontend and backend in both directions:

Frontend → Backend

When you call $postx(), Gale serializes your entire Alpine x-data state and sends it as JSON.

Access it via request()->state()

Backend → Frontend

Call gale()->state() to push updates back. Changes are streamed via SSE and merged into Alpine's reactive state.

Alpine's reactivity automatically updates the UI.

RFC 7386 JSON Merge Patch
State updates use RFC 7386 merge semantics. Only the keys you specify are updated—existing state is preserved.

Reading State

Access the frontend's Alpine state using the request()->state() macro.

Get All State

Call without arguments to get the entire state object:

1// Frontend: x-data="{ count: 5, user: { name: 'John' } }"
2
3$state = request()->state();
4// Returns: ['count' => 5, 'user' => ['name' => 'John']]

Get a Single Value

Pass a key to get a specific value, with an optional default:

1// Get a value
2$count = request()->state('count');
3
4// With a default if the key doesn't exist
5$count = request()->state('count', 0);
6$page = request()->state('page', 1);

Dot Notation

Access nested values using Laravel's familiar dot notation:

1// Frontend: x-data="{ user: { profile: { name: 'John', email: null } } }"
2
3$name = request()->state('user.profile.name');
4// Returns: 'John'
5
6$email = request()->state('user.profile.email', 'guest@example.com');
7// Returns default since email is null
Tip
The state() method uses Laravel's data_get() helper under the hood, so you get the same behavior you're used to.

Controlling What Gets Sent

By default, Gale sends your entire x-data state to the server. However, you often need to keep some state browser-only or selectively include/exclude properties.

Browser-Only State

Properties prefixed with an underscore (_) are never sent to the server. Use this for UI state that doesn't need backend processing:

1<div x-data="{
2    name: '',              // Sent to server
3    email: '',             // Sent to server
4    _showDropdown: false,  // Browser only (underscore prefix)
5    _editMode: false,      // Browser only
6    _cachedResults: []     // Browser only
7}">
Common Browser-Only State
Use underscores for: dropdown visibility, modal states, hover effects, local caches, animation flags, and any UI-only toggles that the server doesn't need to know about.

Include / Exclude Options

For more control, use the include or exclude options in your HTTP magics:

 1<!-- Include: Only send these specific properties -->
 2<button @click="$postx('/save', { include: ['name', 'email'] })">
 3    Save Contact
 4</button>
 5
 6<!-- Exclude: Send everything except these properties -->
 7<button @click="$postx('/save', { exclude: ['password', 'tempData'] })">
 8    Save User
 9</button>

Including Other Components

By default, only the requesting component's state is sent. To include state from other named components:

 1<!-- Include all state from named components -->
 2<button @click="$postx('/checkout', {
 3    includeComponents: ['cart', 'shipping']
 4})">
 5    Checkout
 6</button>
 7
 8<!-- Include only specific keys from components -->
 9<button @click="$postx('/checkout', {
10    includeComponents: {
11        cart: ['items', 'total'],     // Only these keys
12        shipping: true                 // All keys
13    }
14})">
15    Checkout
16</button>

On the server, access component state via request()->state('_components.cart.items').

Note
See State Serialization for complete details on what gets sent and how to control it.

Updating State

Use gale()->state() to push state updates to the frontend.

Single Value

1return gale()->state('count', 42);

Multiple Values

Pass an associative array to update multiple keys at once:

1return gale()->state([
2    'count' => 42,
3    'status' => 'complete',
4    'loading' => false,
5]);

Nested Updates

Update nested properties using dot notation or nested arrays:

1// Using dot notation (updates only the specified key)
2return gale()
3    ->state('user.profile.name', 'Jane')
4    ->state('settings.theme', 'dark');
5
6// Using nested arrays (RFC 7386 merge)
7return gale()->state([
8    'user' => [
9        'profile' => ['name' => 'Jane']  // Other profile keys preserved
10    ]
11]);

Chaining

All gale() methods return the response builder for fluent chaining:

1return gale()
2    ->state('count', $count + 1)
3    ->state('lastUpdated', now()->toISOString())
4    ->state('items', $items->toArray())
5    ->append('#list', $html);  // Can mix with DOM methods

RFC 7386 Merge Semantics

Understanding how state merges is crucial. Here are the key rules:

Current State Patch Sent Result Rule
{ a: 1 } { b: 2 } { a: 1, b: 2 } New keys added
{ a: 1 } { a: 2 } { a: 2 } Existing keys updated
{ a: 1, b: 2 } { a: null } { b: 2 } null deletes key
{ a: { b: 1 } } { a: { c: 2 } } { a: { b: 1, c: 2 } } Objects merge recursively
{ a: [1, 2] } { a: [3] } { a: [3] } Arrays replaced!
Arrays Are Replaced, Not Merged
This is the most common gotcha. Arrays are completely replaced. If you need to add to an array, read the current array first, modify it, then send the complete new array.

Working with Arrays

Here's the correct pattern for adding items to an array:

1// Get current items from frontend state
2$items = request()->state('items', []);
3
4// Add the new item
5$items[] = ['id' => uniqid(), 'title' => $title];
6
7// Send the complete new array
8return gale()->state('items', $items);

Alternatively, use DOM manipulation (append(), prepend()) to add HTML without managing array state.

Deleting State

Remove keys from frontend state using forget():

1// Delete a single key
2return gale()->forget('temporaryData');
3
4// Delete multiple keys
5return gale()->forget(['error', 'warning', 'tempValue']);
6
7// Delete nested keys
8return gale()->forget('form.errors');
How Deletion Works
Under the hood, forget() sends a null value. Per RFC 7386, null values remove keys from the target object.

Conditional Updates

Only If Missing

Set state only if the key doesn't already exist on the frontend:

1// Only sets 'defaults' if it doesn't exist in frontend state
2return gale()->state('defaults', $config, ['onlyIfMissing' => true]);

This is useful for setting initial/default values without overwriting user modifications.

Targeting Specific Components

By default, state updates go to the component that made the request. To update a different component, use named components:

1<!-- Frontend: Register component with x-component -->
2<div x-data="{ items: [], total: 0 }" x-component="cart">
3    ...
4</div>
1// Backend: Update the 'cart' component specifically
2return gale()->componentState('cart', [
3    'items' => $cartItems,
4    'total' => $cartTotal,
5]);
Tip
See the Component Registry documentation for more advanced component operations.

Practical Example

Here's a complete example showing a search feature with state management:

Frontend

 1<div x-data="{ query: '', results: [], searching: false }">
 2    <input type="text"
 3           x-model="query"
 4           @input.debounce.300ms="$postx('/search')"
 5           placeholder="Search...">
 6
 7    <span x-show="searching">Searching...</span>
 8
 9    <ul>
10        <template x-for="result in results" :key="result.id">
11            <li x-text="result.title"></li>
12        </template>
13    </ul>
14</div>

Backend

 1Route::post('/search', function () {
 2    $query = request()->state('query', '');
 3
 4    if (strlen($query) < 2) {
 5        return gale()->state('results', []);
 6    }
 7
 8    $results = Product::query()
 9        ->where('title', 'like', "%{$query}%")
10        ->limit(10)
11        ->get(['id', 'title']);
12
13    return gale()->state('results', $results);
14});

Quick Reference

Method Description
request()->state() Get all frontend state as array
request()->state('key', $default) Get specific key with optional default
gale()->state('key', $value) Update a single state key
gale()->state([...]) Update multiple state keys
gale()->forget('key') Delete state key(s)
gale()->componentState('name', [...]) Update a named component's state

Best Practices

Keep State Minimal

Only include what's necessary in x-data. Large state objects increase request payload size.

Always Use Defaults

Provide defaults with request()->state('key', default) to handle missing keys gracefully.

Validate User Input

Never trust frontend state. Use request()->validateState() for any user-provided data.

Batch Related Updates

Group related state changes in a single state([]) call for cleaner code and fewer SSE events.

On this page