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
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']]
// Frontend: x-data="{ count: 5, user: { name: 'John' } }"
$state = request()->state();
// 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);
// Get a value
$count = request()->state('count');
// With a default if the key doesn't exist
$count = request()->state('count', 0);
$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
// Frontend: x-data="{ user: { profile: { name: 'John', email: null } } }"
$name = request()->state('user.profile.name');
// Returns: 'John'
$email = request()->state('user.profile.email', 'guest@example.com');
// Returns default since email is null
Tip
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}">
<div x-data="{
name: '', // Sent to server
email: '', // Sent to server
_showDropdown: false, // Browser only (underscore prefix)
_editMode: false, // Browser only
_cachedResults: [] // Browser only
}">
Common Browser-Only State
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>
<!-- Include: Only send these specific properties -->
<button @click="$postx('/save', { include: ['name', 'email'] })">
Save Contact
</button>
<!-- Exclude: Send everything except these properties -->
<button @click="$postx('/save', { exclude: ['password', 'tempData'] })">
Save User
</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>
<!-- Include all state from named components -->
<button @click="$postx('/checkout', {
includeComponents: ['cart', 'shipping']
})">
Checkout
</button>
<!-- Include only specific keys from components -->
<button @click="$postx('/checkout', {
includeComponents: {
cart: ['items', 'total'], // Only these keys
shipping: true // All keys
}
})">
Checkout
</button>
On the server, access component state via request()->state('_components.cart.items').
Note
Updating State
Use gale()->state() to push state updates to the frontend.
Single Value
1return gale()->state('count', 42);
return 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]);
return gale()->state([
'count' => 42,
'status' => 'complete',
'loading' => false,
]);
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]);
// Using dot notation (updates only the specified key)
return gale()
->state('user.profile.name', 'Jane')
->state('settings.theme', 'dark');
// Using nested arrays (RFC 7386 merge)
return gale()->state([
'user' => [
'profile' => ['name' => 'Jane'] // Other profile keys preserved
]
]);
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
return gale()
->state('count', $count + 1)
->state('lastUpdated', now()->toISOString())
->state('items', $items->toArray())
->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
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);
// Get current items from frontend state
$items = request()->state('items', []);
// Add the new item
$items[] = ['id' => uniqid(), 'title' => $title];
// Send the complete new array
return 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');
// Delete a single key
return gale()->forget('temporaryData');
// Delete multiple keys
return gale()->forget(['error', 'warning', 'tempValue']);
// Delete nested keys
return gale()->forget('form.errors');
How Deletion Works
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]);
// Only sets 'defaults' if it doesn't exist in frontend state
return 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>
<!-- Frontend: Register component with x-component -->
<div x-data="{ items: [], total: 0 }" x-component="cart">
...
</div>
1// Backend: Update the 'cart' component specifically
2return gale()->componentState('cart', [
3 'items' => $cartItems,
4 'total' => $cartTotal,
5]);
// Backend: Update the 'cart' component specifically
return gale()->componentState('cart', [
'items' => $cartItems,
'total' => $cartTotal,
]);
Tip
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>
<div x-data="{ query: '', results: [], searching: false }">
<input type="text"
x-model="query"
@input.debounce.300ms="$postx('/search')"
placeholder="Search...">
<span x-show="searching">Searching...</span>
<ul>
<template x-for="result in results" :key="result.id">
<li x-text="result.title"></li>
</template>
</ul>
</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});
Route::post('/search', function () {
$query = request()->state('query', '');
if (strlen($query) < 2) {
return gale()->state('results', []);
}
$results = Product::query()
->where('title', 'like', "%{$query}%")
->limit(10)
->get(['id', 'title']);
return gale()->state('results', $results);
});
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.