Request Cycle
Understanding how a Hyper request flows from the browser to your Laravel server and back is fundamental to building reactive applications. This page walks through the complete lifecycle of a Hyper interaction, from a user clicking a button to the interface updating automatically.
The Complete Cycle
A Hyper interaction follows this sequence:
- User Interaction - User clicks a button, submits a form, or types in an input
- Event Capture - Datastar captures the event through
data-on:*attributes - Signal Collection - Non-local signals are gathered from the page
- HTTP Request - Datastar sends a request to your Laravel route with signals in the body
- Request Detection - Laravel identifies it as a Hyper request
- Signal Reading - Your controller accesses signals using
signals() - Business Logic - Validation, database operations, processing
- Response Building -
hyper()methods create the response - SSE Transmission - Server-Sent Events stream back to the browser
- DOM Updates - Datastar patches HTML and updates signals
- Reactive Updates - All dependent elements update automatically
Let's examine each step in detail.
Step 1: User Interaction
The cycle begins when a user interacts with your interface:
<button data-on:click="@postx('/todos')">
Create Todo
</button>When clicked, Datastar's event listener is triggered. The data-on:click attribute tells Datastar what action to perform.
Step 2: Event Capture
Datastar captures the event and evaluates the expression. The @postx directive (a Hyper extension) expands to an action that sends a POST request with CSRF protection:
<!-- What the browser sees after Blade compilation -->
<button data-on:click="@post('/todos')">
Create Todo
</button>Datastar prepares to send the request to /todos.
Step 3: Signal Collection
Before sending the request, Datastar collects all signals from the page, excluding local signals (those starting with _):
<div @signals(['title' => '', 'completed' => false, '_editing' => false])>
<!-- Datastar will send: { title: '', completed: false } -->
<!-- _editing stays in browser only -->
</div>These signals are serialized into JSON format.
Step 4: HTTP Request
Datastar sends an HTTP request to your Laravel route:
Request Headers:
POST /todos HTTP/1.1
Content-Type: application/json
Datastar-Request: true
X-CSRF-TOKEN: [token]Request Body:
{
"title": "Buy groceries",
"completed": false
}The Datastar-Request header identifies this as a Hyper request, not a traditional page load.
Step 5: Request Detection
Your Laravel application receives the request. Hyper provides the request()->isHyper() macro to detect Hyper requests:
public function store()
{
if (request()->isHyper()) {
// Handle reactive request
return hyper()->fragment('todos.list', 'todo-list', $data);
}
// Handle traditional page load
return view('todos.index', $data);
}This enables progressive enhancement—the same route works for both Hyper and traditional requests.
Step 6: Signal Reading
Access signal data using the signals() helper:
public function store()
{
// Get individual signal
$title = signals('title');
// Get all signals
$data = signals()->all();
// Get with default value
$completed = signals('completed', false);
}The signals() helper provides a clean interface to the request's signal payload.
Step 7: Business Logic
Process the request using your existing Laravel code:
public function store()
{
$validated = signals()->validate([
'title' => 'required|string|max:255',
'completed' => 'boolean',
]);
$todo = Todo::create($validated);
return hyper()->signals(['message' => 'Todo created']);
}Validation, database operations, and business logic work exactly as they do in traditional Laravel applications.
Step 8: Response Building
Build your response using the hyper() helper, which returns a fluent HyperResponse instance:
return hyper()
->signals(['title' => '', 'completed' => false])
->fragment('todos.list', 'todo-list', ['todos' => Todo::all()])
->js("showToast('Todo created successfully')");Each method call adds an event to the response. The response builder is chainable, letting you compose multiple updates.
Step 9: SSE Transmission
The response is converted to Server-Sent Events (SSE) format and streamed to the browser:
event: datastar-patch-signals
data: signals {"title":"","completed":false}
event: datastar-patch-elements
data: selector #todo-list
data: mode outer
data: elements <div id="todo-list">
data: elements <div>Updated todo list</div>
data: elements </div>
event: datastar-patch-elements
data: selector body
data: mode append
data: elements <script data-effect="el.remove()">showToast('Todo created successfully')</script>SSE keeps the connection open, allowing multiple events in a single response.
Step 10: DOM Updates
Datastar receives the events and processes them:
Signal Updates:
// datastar-patch-signals event updates the signal store
{ title: '', completed: false }HTML Patches:
<!-- datastar-patch-elements event finds #todo-list and morphs its content -->
<div id="todo-list">
<div>Updated todo list</div>
</div>Script Execution:
// Script runs, then removes itself
showToast('Todo created successfully')Datastar uses DOM morphing (via idiomorph) to intelligently update only what changed, preserving focus, selections, and event listeners.
Step 11: Reactive Updates
Any element that depends on updated signals automatically re-renders:
<div @signals(['count' => 5])>
<p data-text="$count"></p>
<!-- Automatically shows: 5 -->
<p data-text="'Items: ' + $count"></p>
<!-- Automatically shows: Items: 5 -->
<div data-show="$count > 0">
You have items!
</div>
<!-- Automatically shown because count > 0 -->
</div>When count changes from a server response, all three elements update without additional code.
Request Details
Hyper Request Headers
Every Hyper request includes:
Datastar-Request: true Identifies the request as a Hyper request. Check with request()->isHyper().
X-CSRF-TOKEN: [token] CSRF protection for mutating operations (@postx, @putx, @patchx, @deletex).
Navigation Request Headers
When using client-side navigation with data-navigate or @navigate, additional headers are sent:
HYPER-NAVIGATE: true Identifies this as a navigation request.
HYPER-NAVIGATE-KEY: sidebar The navigation key for targeted updates. Check with request()->isHyperNavigate('sidebar').
Signal Transmission Format
Signals are sent in the request body as JSON:
{
"username": "john_doe",
"email": "john@example.com",
"userId_": 123
}Locked signals (ending with _) are validated against encrypted session data to prevent tampering.
Response Details
SSE Event Format
Hyper responses use Server-Sent Events with this structure:
event: [event-type]
data: [line1]
data: [line2]Each event type corresponds to a Datastar action:
datastar-patch-signals - Update reactive signals datastar-patch-elements - Update DOM elements datastar-remove-elements - Remove DOM elements
Signal Merge Behavior
When you send signal updates, they merge with existing signals:
// Existing signals: { count: 5, message: 'Hello' }
return hyper()->signals(['count' => 10]);
// Result: { count: 10, message: 'Hello' }
// Only count changed, message preservedTo delete a signal, set it to null:
return hyper()->signals(['message' => null]);
// Result: { count: 10 }
// message signal deletedDOM Morphing vs Replacing
By default, Datastar morphs the DOM (mode: outer), preserving:
- Element focus
- Text selections
- CSS transitions
- Event listeners
For a complete replacement (mode: replace), specify it in the options:
return hyper()->fragment('view', 'fragment', $data, ['mode' => 'replace']);Request Macros
Hyper adds several macros to Laravel's Request class for detecting and working with Hyper requests.
request()->isHyper()
Check if the current request is a Hyper request:
if (request()->isHyper()) {
return hyper()->fragment('todos.list', 'list', $data);
}
return view('todos.index', $data);This checks for the Datastar-Request header.
request()->signals()
Access signals from the request:
// Get HyperSignal instance
$hyperSignal = request()->signals();
// Get specific signal
$title = request()->signals('title');
// Get with default
$completed = request()->signals('completed', false);This is an alias for the signals() helper.
request()->isHyperNavigate()
Check if the request is from client-side navigation:
if (request()->isHyperNavigate()) {
// Navigation request - send fragment
return hyper()->fragment('products.index', 'product-list', $data);
}
// Direct page load - send full view
return view('products.index', $data);Check for a specific navigation key:
if (request()->isHyperNavigate('sidebar')) {
// Update only sidebar
return hyper()->fragment('layout', 'sidebar', $data);
}
if (request()->isHyperNavigate('pagination')) {
// Update list with pagination
return hyper()->fragment('products.index', 'list-with-pages', $data);
}Check for multiple navigation keys:
if (request()->isHyperNavigate(['sidebar', 'header'])) {
// Request includes sidebar OR header key
}request()->hyperNavigateKey()
Get the navigation key as a string:
$key = request()->hyperNavigateKey();
// Returns: 'sidebar' or nullrequest()->hyperNavigateKeys()
Get all navigation keys as an array:
$keys = request()->hyperNavigateKeys();
// Returns: ['sidebar', 'header'] or []Debugging the Cycle
Browser DevTools
Network Tab:
- Filter by "Fetch/XHR" or "EventStream"
- Click on the request to
/todos - Headers tab shows
Datastar-Request: true - Payload tab shows the JSON signal data
- Response tab shows SSE events
Console Tab:
// View all signals
console.log($$signals)
// Watch signal changes
$$signalsServer-Side Logging
Log request details in your controller:
public function store()
{
logger('Hyper request received', [
'is_hyper' => request()->isHyper(),
'signals' => signals()->all(),
'is_navigate' => request()->isHyperNavigate(),
'navigate_key' => request()->hyperNavigateKey(),
]);
// ... rest of logic
}Common Issues
Signals not updating:
- Check that signal names match between frontend and backend
- Verify the signal isn't local (doesn't start with
_) - Ensure the response is actually being sent (
return hyper()->signals(...))
DOM not patching:
- Verify the fragment selector matches an element ID
- Check that the HTML structure hasn't changed drastically
- Try using
mode: 'replace'instead of morphing
CSRF token mismatch:
- Ensure
@hyperdirective is included in your layout - Use
@postx,@putx,@patchx,@deletexfor mutating operations - Check that your session is configured correctly
Request not detected as Hyper:
- Verify
@hyperis included (initializes Datastar) - Check browser console for JavaScript errors
- Ensure you're using Hyper actions (
@postx,@get, etc.)

