Skip to content

Responses

Responses are how your Laravel backend communicates with the reactive frontend. When a user triggers an action, your controller processes the request and returns a response that updates signals, patches the DOM, executes JavaScript, or navigates the browser. The hyper() response builder provides a fluent, Laravel-familiar API for building these reactive responses.

The hyper() Helper

The hyper() helper function returns a HyperResponse instance that implements Laravel's Responsable interface. This means it integrates seamlessly with Laravel's response system while providing reactive capabilities.

php
use function Dancycodes\Hyper\hyper;

public function increment()
{
    $count = signals('count', 0) + 1;

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

How it works:

  1. You call hyper() to get a response builder instance
  2. Chain methods to build your response (signals, DOM updates, navigation, etc.)
  3. Return the instance from your controller
  4. Laravel converts it to a Server-Sent Events (SSE) stream
  5. Datastar processes the events and updates the frontend

Key insight: The hyper() builder uses a fluent interface, so you can chain multiple operations together. Each method returns $this, allowing you to build complex responses in a single, readable chain.

Request-Scoped Singleton

Here's a critical concept that unlocks powerful patterns in Hyper: hyper() returns the same instance throughout a single request.

This means you can call hyper() anywhere in your application code—controllers, service classes, helper functions, even validation callbacks—and all operations accumulate on the same response instance. When you finally return the hyper() instance, all accumulated events are sent together as a single atomic update.

Accumulation in Action

php
public function processOrder()
{
    // First call - updates signals
    hyper()->signals(['status' => 'Processing...']);

    // Process payment in a service class
    $this->paymentService->charge($amount);

    // Service class also calls hyper()
    // class PaymentService {
    //     public function charge($amount) {
    //         // This adds to the SAME instance
    //         hyper()->signals(['paymentStatus' => 'charged']);
    //     }
    // }

    // Back in controller - add more updates
    hyper()->fragment('orders.receipt', 'receipt', $data);

    // When you return, ALL events are sent together
    return hyper()->js("showToast('Order complete!')");
}

What gets sent to the browser:

event: datastar-patch-signals
data: signals {"status":"Processing..."}

event: datastar-patch-signals
data: signals {"paymentStatus":"charged"}

event: datastar-patch-elements
data: elements <div>Receipt HTML</div>
...

event: datastar-patch-elements
data: elements <script>showToast('Order complete!')</script>
...

All four operations—even though called at different points in your code—are sent as a single, atomic response.

Practical Patterns

Service Layer Pattern:

php
class TodoService
{
    public function createTodo(array $data): Todo
    {
        $todo = Todo::create($data);

        // Service updates signals directly
        hyper()->signals([
            'todoCount' => Todo::count(),
            'latestTodo' => $todo->toArray()
        ]);

        return $todo;
    }
}

// In your controller
public function store(TodoService $service)
{
    $validated = signals()->validate(['title' => 'required']);

    $todo = $service->createTodo($validated);

    // Your controller adds its own updates
    return hyper()
        ->signals(['title' => '', 'errors' => []])
        ->fragment('todos.list', 'list', ['todos' => Todo::all()]);

    // Both service and controller updates are sent together!
}

Helper Function Pattern:

php
// In app/helpers.php
function flashSuccess(string $message)
{
    hyper()->signals(['flash' => ['type' => 'success', 'message' => $message]]);
}

function flashError(string $message)
{
    hyper()->signals(['flash' => ['type' => 'error', 'message' => $message]]);
}

// In your controller
public function update($id)
{
    $todo = Todo::findOrFail($id);
    $todo->update(signals()->only(['title']));

    flashSuccess('Todo updated!');

    return hyper()->fragment('todos.item', 'item', ['todo' => $todo]);

    // Flash helper's signal update + fragment update = sent together
}

Model Observer Pattern:

php
class TodoObserver
{
    public function created(Todo $todo)
    {
        hyper()->signals(['totalTodos' => Todo::count()]);
    }

    public function deleted(Todo $todo)
    {
        hyper()->signals(['totalTodos' => Todo::count()]);
    }
}

// In your controller
public function destroy($id)
{
    Todo::findOrFail($id)->delete();
    // Observer already updated totalTodos signal

    return hyper()
        ->fragment('todos.list', 'list', ['todos' => Todo::all()])
        ->signals(['message' => 'Deleted successfully']);

    // Observer + controller updates = sent together
}

Validation Callback Pattern:

php
public function store()
{
    $validated = signals()->validate([
        'email' => [
            'required',
            'email',
            function ($attribute, $value, $fail) {
                if (User::where('email', $value)->exists()) {
                    // Update signals in validation callback
                    hyper()->signals(['emailSuggestion' => $value . '.backup']);
                    $fail('Email already taken. Try ' . $value . '.backup');
                }
            }
        ]
    ]);

    // If validation passes
    return hyper()->signals(['success' => true, 'errors' => []]);

    // If validation fails, suggestion signal + errors are sent together
}

Middleware Pattern

You can even call hyper() in middleware:

php
class TrackUserActivity
{
    public function handle($request, $next)
    {
        $response = $next($request);

        if ($request->isHyper()) {
            // Add activity tracking to any Hyper response
            hyper()->signals(['lastActivity' => now()->toIso8601String()]);
        }

        return $response;
    }
}

The middleware's signal update is automatically included when the controller returns.

Important Notes

Only one return matters:

You only need to return hyper() once at the end of your controller. All previous calls accumulate events, but the return is what triggers the response:

php
public function process()
{
    hyper()->signals(['step' => 1]);

    processData();

    hyper()->signals(['step' => 2]);

    // Only this return matters - but it includes BOTH signals updates
    return hyper()->signals(['step' => 3]);
}

Streaming mode changes behavior:

When you call hyper()->stream(), the behavior changes—events are sent immediately instead of accumulated. We'll cover this in Backend Patterns: Streaming.

New instance per request:

Each HTTP request gets a fresh HyperResponse instance. Events don't carry over between requests.

Why This Matters

This singleton pattern enables clean separation of concerns. Your business logic can update signals without knowing about the view layer, and your controller can focus on orchestration without managing state in multiple places. Everything comes together automatically when you return the response.

It's a powerful feature that makes Hyper feel natural in Laravel applications while maintaining the reactive programming model.

Signal Updates

The most common operation is updating signals. Signals changed on the server automatically update on the client, triggering reactive UI updates.

Basic Signal Updates

php
// Update a single signal
return hyper()->signals(['count' => 5]);

// Alternative syntax for single signal
return hyper()->signals('count', 5);

// Update multiple signals
return hyper()->signals([
    'count' => 5,
    'message' => 'Updated successfully!',
    'timestamp' => now()->toIso8601String()
]);

Forgetting Signals

Sometimes you need to remove signals entirely, not just set them to null:

php
// Forget specific signals
return hyper()->forget('tempData');

// Forget multiple signals
return hyper()->forget(['temp1', 'temp2', 'cached']);

// Forget all signals
return hyper()->forget();

Locked Signals

Locked signals (ending with _) are automatically validated and managed by Hyper. You update them the same way as regular signals:

php
return hyper()->signals([
    'userId_' => auth()->id(),      // Locked signal
    'role_' => auth()->user()->role  // Locked signal
]);

Hyper automatically:

  • Encrypts and stores locked signals in the session on first call
  • Validates them on subsequent requests
  • Throws HyperSignalTamperedException if tampering is detected

We'll cover locked signals in detail in Advanced: Locked Signals.

View Rendering

Hyper can render complete Blade views and patch them into your page. This is useful for updating entire sections with complex HTML.

Rendering Full Views

php
public function loadDashboard()
{
    $stats = Stats::forUser(auth()->user());

    return hyper()->view('dashboard.stats', ['stats' => $stats]);
}

By default, view() renders a view template and passes data to it. The first parameter is the view path (e.g., 'dashboard.stats' references dashboard/stats.blade.php), and the second parameter is an array of data to make available in that template.

With explicit targeting:

php
return hyper()->view('dashboard.stats', $data, [
    'selector' => '#dashboard-content',
    'mode' => 'inner'  // Update innerHTML only
]);

Rendering HTML Directly

For simple HTML updates without creating a separate view file:

php
return hyper()->html('<div class="alert">Updated!</div>', [
    'selector' => '#notification',
    'mode' => 'inner'
]);

Working with Fragments

Fragments let you render specific portions of a Blade view without creating separate files. They're defined with @fragment directives:

blade
{{-- resources/views/todos/index.blade.php --}}
<div id="todos-page">
    <h1>Todos</h1>

    @fragment('todo-list')
    <div id="todo-list">
        @foreach($todos as $todo)
            <x-todo-item :todo="$todo" />
        @endforeach
    </div>
    @endfragment

    @fragment('todo-stats')
    <div id="todo-stats">
        <p>{{ $todos->count() }} total todos</p>
    </div>
    @endfragment
</div>

Rendering a single fragment:

php
public function updateTodos()
{
    $todos = Todo::where('user_id', auth()->id())->get();

    return hyper()->fragment('todos.index', 'todo-list', ['todos' => $todos]);
}

The fragment is automatically targeted using its ID (#todo-list in this case) found in the view path todos/index.

Rendering multiple fragments:

php
public function updateTodosAndStats()
{
    $todos = Todo::where('user_id', auth()->id())->get();

    return hyper()->fragments([
        ['view' => 'todos.index', 'fragment' => 'todo-list', 'data' => ['todos' => $todos]],
        ['view' => 'todos.index', 'fragment' => 'todo-stats', 'data' => ['todos' => $todos]]
    ]);
}

With custom targeting:

php
return hyper()->fragment('todos.index', 'todo-list', $data, [
    'selector' => '#custom-container',
    'mode' => 'append'  // Append instead of replace
]);

We'll cover fragments in depth in Backend Patterns: Fragments.

DOM Manipulation

Hyper provides methods that correspond to Datastar's patch modes, giving you precise control over how HTML is inserted into the page.

Patch Modes

Outer HTML (default):

Replaces the entire element including its tags. This mode uses intelligent morphing to preserve JavaScript state:

php
return hyper()->outer('#notification', '<div id="notification">Updated!</div>');

Inner HTML:

Replaces only the contents inside the element, keeping the element itself:

php
return hyper()->inner('#notification', '<p>Updated content</p>');

Append:

Adds content as the last child of the target element:

php
return hyper()->append('#messages', '<div class="message">New message</div>');

Prepend:

Adds content as the first child of the target element:

php
return hyper()->prepend('#messages', '<div class="message">Newest message</div>');

Before:

Inserts content immediately before the target element (as a sibling):

php
return hyper()->before('#submit-btn', '<button>Preview</button>');

After:

Inserts content immediately after the target element (as a sibling):

php
return hyper()->after('#header', '<div class="breadcrumb">Home / Page</div>');

Replace:

Completely replaces the target element without morphing (useful when morphing causes issues):

php
return hyper()->replace('#widget', '<div>New widget</div>');

Remove:

Removes elements from the DOM:

php
return hyper()->remove('#temporary-element');

// Remove multiple elements
return hyper()->remove('.notification');

Understanding Morphing

By default, Hyper uses Datastar's intelligent DOM morphing when updating elements. Morphing updates the DOM while preserving:

  • JavaScript event listeners
  • Input focus and selection
  • Form state
  • CSS transitions and animations
  • Component state

When morphing happens:

php
// Outer mode (default) - uses morphing
hyper()->fragment('view', 'fragment', $data)

// Explicit outer mode - uses morphing
hyper()->outer('#element', '<div id="element">Updated</div>')

When to use replace instead:

php
// Replace mode - no morphing, complete replacement
hyper()->replace('#element', '<div>New element</div>')

Use replace when:

  • The element structure changes drastically
  • You're replacing with a completely different component
  • Morphing is causing unexpected behavior

We'll cover morphing in detail in Advanced: DOM Morphing.

JavaScript Execution

Sometimes you need to run custom JavaScript as part of your response. The js() method executes JavaScript code in the browser:

php
return hyper()
    ->signals(['saved' => true])
    ->js('console.log("Saved successfully!")');

With multiple operations:

php
return hyper()
    ->signals(['count' => $count])
    ->js('
        console.log("Updated count to: " + ' . $count . ');
        if (' . $count . ' >= 10) {
            alert("You reached 10!");
        }
    ');

Common use cases:

php
// Focus an input
hyper()->js('document.getElementById("search").focus()')

// Scroll to element
hyper()->js('document.getElementById("error").scrollIntoView()')

// Trigger third-party library
hyper()->js('window.updateChart()')

// Flash effect
hyper()->js('
    document.body.style.backgroundColor = "#4ade80";
    setTimeout(() => { document.body.style.backgroundColor = ""; }, 300);
')

Alias: You can also use script() if you prefer:

php
return hyper()->script('console.log("Hello!")');

Client-Side Navigation

Hyper's navigation methods enable SPA-like client-side navigation without full page reloads. Navigation updates the browser URL and requests partial content from the server.

Basic Navigation

php
// Simple navigation
return hyper()->navigate('/dashboard');

// With navigation key (for targeted updates)
return hyper()->navigate('/dashboard', 'main');

The navigation key ('main') lets your controller check which part of the page requested navigation:

php
public function dashboard()
{
    $data = ['stats' => Stats::get()];

    if (request()->isHyperNavigate('main')) {
        // Return just the main content
        return hyper()->fragment('dashboard', 'content', $data);
    }

    // Return full page for initial load
    return view('dashboard', $data);
}

Query Parameter Handling

One of Hyper's most powerful features is smart query parameter merging during navigation.

Clean navigation (default):

Navigates to the URL without preserving current query parameters:

php
// Current URL: /products?search=phone&category=electronics&page=2
return hyper()->navigateClean('/products?sort=price');
// Result: /products?sort=price (search, category, page removed)

Merge navigation:

Preserves all current query parameters and merges with new ones:

php
// Current URL: /products?search=phone&category=electronics&page=2
return hyper()->navigateMerge('/products?sort=price');
// Result: /products?search=phone&category=electronics&page=2&sort=price

Preserve specific parameters:

php
// Current URL: /products?search=phone&category=electronics&page=2
return hyper()->navigateOnly('/products?page=1', ['search', 'category']);
// Result: /products?search=phone&category=electronics&page=1

Preserve all except specific:

php
// Current URL: /products?search=phone&category=electronics&page=2
return hyper()->navigateExcept('/products', ['page']);
// Result: /products?search=phone&category=electronics

JSON Query Navigation

You can navigate using query parameter objects instead of URL strings:

php
// Navigate to current path with new query parameters
return hyper()->navigate(['search' => 'laptop', 'category' => 'computers']);
// Result: /products?search=laptop&category=computers

// With merge
return hyper()->navigate(['page' => 2], 'pagination', ['merge' => true]);
// Preserves other parameters, just updates page

Clearing parameters:

php
// Clear specific parameters
return hyper()->navigate(['search' => null, 'category' => null], 'clear');

// Or use clearQueries helper
return hyper()->clearQueries(['search', 'category']);

Hyper provides several convenience methods for common navigation patterns:

Update query parameters:

php
// Update/add query parameters on current page
return hyper()->updateQueries(['filter' => 'active', 'sort' => 'date']);

Reset pagination:

php
// Go to page 1 while keeping other filters
return hyper()->resetPagination();

Explicit merge control:

php
// Navigate with explicit merge setting
return hyper()->navigateWith('/products?view=grid', 'view', true);
// Third parameter: true = merge, false = clean

Replace instead of push:

php
// Use replaceState instead of pushState (doesn't add history entry)
return hyper()->navigateReplace('/products');

We'll cover navigation extensively in the Navigation section.

Browser URL Management

Sometimes you want to update the browser URL without navigation. This is useful for reflecting state changes in the URL while keeping the same page content.

Push vs Replace

Push URL (adds to history):

php
return hyper()->pushUrl('/products?view=grid');

The browser's back button will return to the previous URL.

Replace URL (replaces current entry):

php
return hyper()->replaceUrl('/products?view=grid');

The browser's back button skips this URL change.

Using Routes

php
// Push with route
return hyper()->pushRoute('products.index', ['category' => 'electronics']);

// Replace with route
return hyper()->replaceRoute('products.show', ['product' => $product->id]);

URL from Query Parameters

php
// Update just the query parameters
return hyper()->pushUrl(['filter' => 'active', 'sort' => 'date']);
// Result: /current-path?filter=active&sort=date

Conditional Responses

The when() method executes operations conditionally, keeping your code clean and readable:

php
return hyper()
    ->signals(['count' => $count])
    ->when($count === 10, function ($hyper) {
        return $hyper
            ->signals(['milestone' => 'reached'])
            ->js('alert("You reached 10!")');
    });

Multiple conditions:

php
return hyper()
    ->signals(['score' => $score])
    ->when($score >= 100, function ($hyper) {
        return $hyper->signals(['level' => 'expert']);
    })
    ->when($score >= 50, function ($hyper) use ($score) {
        return $hyper->signals(['level' => 'intermediate']);
    })
    ->when($score < 50, function ($hyper) {
        return $hyper->signals(['level' => 'beginner']);
    });

With else:

php
return hyper()
    ->when($success,
        function ($hyper) {
            return $hyper->signals(['message' => 'Success!']);
        },
        function ($hyper) {
            return $hyper->signals(['error' => 'Failed!']);
        }
    );

Streaming Responses

Streaming lets you send multiple events over time during a long-running operation. This is useful for progress updates, processing queues, or real-time feedback.

Basic Streaming

php
return hyper()->stream(function ($hyper) {
    // First update
    $hyper->signals(['status' => 'Processing...', 'progress' => 0]);
    sleep(1);

    // Second update
    $hyper->signals(['status' => 'Almost done...', 'progress' => 50]);
    sleep(1);

    // Final update
    $hyper->signals(['status' => 'Complete!', 'progress' => 100]);
});

How streaming works:

  1. The stream() method accepts a callback
  2. Inside the callback, each method call sends an event immediately
  3. The browser receives and processes events as they arrive
  4. The UI updates in real-time

Streaming with Loops

php
return hyper()->stream(function ($hyper) {
    $items = Item::all();
    $total = $items->count();

    foreach ($items as $index => $item) {
        processItem($item);

        $progress = (($index + 1) / $total) * 100;

        $hyper->signals([
            'progress' => round($progress),
            'current' => $item->name,
            'processed' => $index + 1,
            'total' => $total
        ]);

        sleep(1); // Small delay to avoid overwhelming the browser
    }

    $hyper->signals(['status' => 'All items processed!']);
});

Exception Handling in Streams

When an exception occurs in a stream, Hyper shows Laravel's native error page:

php
return hyper()->stream(function ($hyper) {
    $hyper->signals(['status' => 'Starting...']);

    try {
        $result = riskyOperation();
        $hyper->signals(['result' => $result]);
    } catch (\Exception $e) {
        // Exception automatically shows Laravel's error page
        throw $e;
    }
});

dd() and dump() in Streams

Calling dd() or dump() in a stream shows Laravel's native dump output and stops execution:

php
return hyper()->stream(function ($hyper) {
    $hyper->signals(['status' => 'Processing...']);

    $data = processData();
    dd($data); // Shows Laravel's dd() page and stops stream
});

redirect() in Streams

Using redirect() in a stream performs a real browser redirect and stops the stream:

php
return hyper()->stream(function ($hyper) {
    $hyper->signals(['status' => 'Processing payment...']);

    if ($paymentSucceeds) {
        redirect()->to('/success'); // Redirects browser and stops stream
    }
});

We'll cover streaming in detail in Backend Patterns: Streaming.

Dual Responses (Progressive Enhancement)

The web() method sets a fallback response for non-Hyper requests, enabling progressive enhancement:

php
public function index()
{
    $contacts = Contact::paginate(10);

    return hyper()
        ->fragment('contacts.index', 'contact-list', compact('contacts'))
        ->web(view('contacts.index', compact('contacts')));
}

How it works:

  • Hyper request: Returns the fragment (partial update)
  • Normal request: Returns the full view (initial page load or JavaScript disabled)

This ensures your application works without JavaScript while providing enhanced UX when Hyper is available.

With multiple operations:

php
return hyper()
    ->signals(['count' => $count])
    ->fragment('dashboard', 'stats', ['stats' => $stats])
    ->web(view('dashboard', ['stats' => $stats, 'count' => $count]));

We'll cover dual responses in Backend Patterns: Dual Responses.

Chaining Operations

The real power of hyper() comes from chaining multiple operations into complex, atomic updates:

php
public function createTodo()
{
    $validated = signals()->validate([
        'title' => 'required|string|max:255'
    ]);

    $todo = Todo::create($validated);
    $todos = Todo::all();

    return hyper()
        // Update signals
        ->signals([
            'title' => '',              // Clear input
            'todoCount' => $todos->count(),
            'message' => 'Todo created!',
            'errors' => []              // Clear errors
        ])
        // Update todo list
        ->fragment('todos.index', 'todo-list', ['todos' => $todos])
        // Show notification
        ->append('#notifications', view('partials.notification', [
            'message' => 'Todo created successfully!'
        ])->render())
        // Execute JavaScript
        ->js('setTimeout(() => document.querySelector(".notification").remove(), 3000)');
}

Each method returns $this, so you can chain as many operations as needed. All operations are sent together in a single response, ensuring atomic updates.

Response Headers

Hyper automatically sets the correct headers for Server-Sent Events:

Cache-Control: no-cache
Content-Type: text/event-stream
X-Accel-Buffering: no
X-Hyper-Response: true
Connection: keep-alive (for HTTP/1.1)

You don't need to set these manually—Hyper handles it automatically when you return a hyper() response.

Short-Circuiting for Non-Hyper Requests

All hyper() methods automatically short-circuit for non-Hyper requests unless you provide a web() fallback:

php
return hyper()->signals(['count' => 5]);
// If not a Hyper request and no web() fallback, throws LogicException

With fallback:

php
return hyper()
    ->signals(['count' => 5])
    ->web(view('page', ['count' => 5]));
// Works for both Hyper and non-Hyper requests

This prevents accidental SSE responses to regular HTTP requests.

Common Patterns

Form Submission with Validation

php
public function store()
{
    $validated = signals()->validate([
        'name' => 'required|string|max:255',
        'email' => 'required|email|unique:users'
    ]);

    User::create($validated);

    return hyper()->signals([
        'name' => '',
        'email' => '',
        'message' => 'User created successfully!',
        'errors' => []
    ]);
}

Paginated List Update

php
public function index()
{
    $page = signals('page', 1);
    $search = signals('search', '');

    $products = Product::query()
        ->when($search, fn($q) => $q->where('name', 'like', "%{$search}%"))
        ->paginate(10, ['*'], 'page', $page);

    return hyper()
        ->fragment('products.index', 'product-list', ['products' => $products])
        ->signals(['totalProducts' => $products->total()]);
}

Multi-Step Process

php
public function processOrder()
{
    return hyper()->stream(function ($hyper) {
        // Step 1: Validate payment
        $hyper->signals(['step' => 1, 'status' => 'Validating payment...']);
        $payment = validatePayment();

        // Step 2: Create order
        $hyper->signals(['step' => 2, 'status' => 'Creating order...']);
        $order = createOrder($payment);

        // Step 3: Send confirmation
        $hyper->signals(['step' => 3, 'status' => 'Sending confirmation...']);
        sendConfirmation($order);

        // Complete
        $hyper->signals([
            'step' => 4,
            'status' => 'Order complete!',
            'orderId' => $order->id
        ]);
    });
}

Conditional Fragment Updates

php
public function update($id)
{
    $todo = Todo::findOrFail($id);
    $todo->update(signals()->only(['title', 'completed']));

    $todos = Todo::all();

    return hyper()
        ->fragment('todos.index', 'todo-list', ['todos' => $todos])
        ->when($todo->completed, function ($hyper) use ($todos) {
            $completedCount = $todos->where('completed', true)->count();
            return $hyper->signals(['completedCount' => $completedCount]);
        });
}

What's Next?

Now that you understand how to build responses with hyper(), explore related topics: