Skip to content

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:

  1. User Interaction - User clicks a button, submits a form, or types in an input
  2. Event Capture - Datastar captures the event through data-on:* attributes
  3. Signal Collection - Non-local signals are gathered from the page
  4. HTTP Request - Datastar sends a request to your Laravel route with signals in the body
  5. Request Detection - Laravel identifies it as a Hyper request
  6. Signal Reading - Your controller accesses signals using signals()
  7. Business Logic - Validation, database operations, processing
  8. Response Building - hyper() methods create the response
  9. SSE Transmission - Server-Sent Events stream back to the browser
  10. DOM Updates - Datastar patches HTML and updates signals
  11. 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:

blade
<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:

blade
<!-- 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 _):

blade
<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:

json
{
    "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:

php
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:

php
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:

php
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:

php
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:

javascript
// datastar-patch-signals event updates the signal store
{ title: '', completed: false }

HTML Patches:

html
<!-- datastar-patch-elements event finds #todo-list and morphs its content -->
<div id="todo-list">
  <div>Updated todo list</div>
</div>

Script Execution:

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

blade
<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).

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:

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:

php
// Existing signals: { count: 5, message: 'Hello' }

return hyper()->signals(['count' => 10]);

// Result: { count: 10, message: 'Hello' }
// Only count changed, message preserved

To delete a signal, set it to null:

php
return hyper()->signals(['message' => null]);

// Result: { count: 10 }
// message signal deleted

DOM 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:

php
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:

php
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:

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

php
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:

php
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:

php
if (request()->isHyperNavigate(['sidebar', 'header'])) {
    // Request includes sidebar OR header key
}

request()->hyperNavigateKey()

Get the navigation key as a string:

php
$key = request()->hyperNavigateKey();
// Returns: 'sidebar' or null

request()->hyperNavigateKeys()

Get all navigation keys as an array:

php
$keys = request()->hyperNavigateKeys();
// Returns: ['sidebar', 'header'] or []

Debugging the Cycle

Browser DevTools

Network Tab:

  1. Filter by "Fetch/XHR" or "EventStream"
  2. Click on the request to /todos
  3. Headers tab shows Datastar-Request: true
  4. Payload tab shows the JSON signal data
  5. Response tab shows SSE events

Console Tab:

javascript
// View all signals
console.log($$signals)

// Watch signal changes
$$signals

Server-Side Logging

Log request details in your controller:

php
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 @hyper directive is included in your layout
  • Use @postx, @putx, @patchx, @deletex for mutating operations
  • Check that your session is configured correctly

Request not detected as Hyper:

  • Verify @hyper is included (initializes Datastar)
  • Check browser console for JavaScript errors
  • Ensure you're using Hyper actions (@postx, @get, etc.)