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.
use function Dancycodes\Hyper\hyper;
public function increment()
{
$count = signals('count', 0) + 1;
return hyper()->signals(['count' => $count]);
}How it works:
- You call
hyper()to get a response builder instance - Chain methods to build your response (signals, DOM updates, navigation, etc.)
- Return the instance from your controller
- Laravel converts it to a Server-Sent Events (SSE) stream
- 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
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:
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:
// 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:
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:
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:
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:
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
// 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:
// 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:
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
HyperSignalTamperedExceptionif 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
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:
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:
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:
{{-- 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:
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:
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:
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:
return hyper()->outer('#notification', '<div id="notification">Updated!</div>');Inner HTML:
Replaces only the contents inside the element, keeping the element itself:
return hyper()->inner('#notification', '<p>Updated content</p>');Append:
Adds content as the last child of the target element:
return hyper()->append('#messages', '<div class="message">New message</div>');Prepend:
Adds content as the first child of the target element:
return hyper()->prepend('#messages', '<div class="message">Newest message</div>');Before:
Inserts content immediately before the target element (as a sibling):
return hyper()->before('#submit-btn', '<button>Preview</button>');After:
Inserts content immediately after the target element (as a sibling):
return hyper()->after('#header', '<div class="breadcrumb">Home / Page</div>');Replace:
Completely replaces the target element without morphing (useful when morphing causes issues):
return hyper()->replace('#widget', '<div>New widget</div>');Remove:
Removes elements from the DOM:
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:
// 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:
// 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:
return hyper()
->signals(['saved' => true])
->js('console.log("Saved successfully!")');With multiple operations:
return hyper()
->signals(['count' => $count])
->js('
console.log("Updated count to: " + ' . $count . ');
if (' . $count . ' >= 10) {
alert("You reached 10!");
}
');Common use cases:
// 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:
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
// 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:
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:
// 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:
// Current URL: /products?search=phone&category=electronics&page=2
return hyper()->navigateMerge('/products?sort=price');
// Result: /products?search=phone&category=electronics&page=2&sort=pricePreserve specific parameters:
// Current URL: /products?search=phone&category=electronics&page=2
return hyper()->navigateOnly('/products?page=1', ['search', 'category']);
// Result: /products?search=phone&category=electronics&page=1Preserve all except specific:
// Current URL: /products?search=phone&category=electronics&page=2
return hyper()->navigateExcept('/products', ['page']);
// Result: /products?search=phone&category=electronicsJSON Query Navigation
You can navigate using query parameter objects instead of URL strings:
// 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 pageClearing parameters:
// Clear specific parameters
return hyper()->navigate(['search' => null, 'category' => null], 'clear');
// Or use clearQueries helper
return hyper()->clearQueries(['search', 'category']);Navigation Convenience Methods
Hyper provides several convenience methods for common navigation patterns:
Update query parameters:
// Update/add query parameters on current page
return hyper()->updateQueries(['filter' => 'active', 'sort' => 'date']);Reset pagination:
// Go to page 1 while keeping other filters
return hyper()->resetPagination();Explicit merge control:
// Navigate with explicit merge setting
return hyper()->navigateWith('/products?view=grid', 'view', true);
// Third parameter: true = merge, false = cleanReplace instead of push:
// 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):
return hyper()->pushUrl('/products?view=grid');The browser's back button will return to the previous URL.
Replace URL (replaces current entry):
return hyper()->replaceUrl('/products?view=grid');The browser's back button skips this URL change.
Using Routes
// 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
// Update just the query parameters
return hyper()->pushUrl(['filter' => 'active', 'sort' => 'date']);
// Result: /current-path?filter=active&sort=dateConditional Responses
The when() method executes operations conditionally, keeping your code clean and readable:
return hyper()
->signals(['count' => $count])
->when($count === 10, function ($hyper) {
return $hyper
->signals(['milestone' => 'reached'])
->js('alert("You reached 10!")');
});Multiple conditions:
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:
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
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:
- The
stream()method accepts a callback - Inside the callback, each method call sends an event immediately
- The browser receives and processes events as they arrive
- The UI updates in real-time
Streaming with Loops
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:
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:
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:
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:
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:
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:
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:
return hyper()->signals(['count' => 5]);
// If not a Hyper request and no web() fallback, throws LogicExceptionWith fallback:
return hyper()
->signals(['count' => 5])
->web(view('page', ['count' => 5]));
// Works for both Hyper and non-Hyper requestsThis prevents accidental SSE responses to regular HTTP requests.
Common Patterns
Form Submission with Validation
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
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
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
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:
- Validation: Deep dive into
signals()->validate()and error handling - Request Cycle: Understand the complete flow from request to response
- Backend Patterns: Fragments: Master fragment rendering and organization
- Backend Patterns: Streaming: Build real-time, long-running operations
- Navigation: Explore client-side navigation in depth

