Signals
Signals are the foundation of reactivity in Hyper applications. They're reactive variables that automatically update your user interface when their values change, eliminating the need for manual DOM manipulation. Understanding signals is essential to building effective Hyper applications.
What Are Signals? (Datastar)
A signal is a reactive variable tracked by Datastar. When a signal's value changes, Datastar automatically updates every element that displays or depends on that signal. Think of signals as your application's reactive state—the single source of truth that drives your interface.
<div data-signals="{count: 0}">
<!-- Signal 'count' is created with initial value 0 -->
<p data-text="$count"></p>
<!-- This paragraph automatically updates when count changes -->
</div>The $ prefix tells Datastar you're referencing a signal, not a regular JavaScript variable. When you write $count, Datastar watches that signal and updates any element using it whenever its value changes.
Creating Signals
There are several ways to create signals, depending on your needs.
Using data-signals (Datastar)
The data-signals attribute creates signals with initial values:
<div data-signals="{
username: 'John',
age: 25,
isActive: true,
tags: ['developer', 'designer']
}">
<!-- Signals are now available throughout the page -->
</div>You can create multiple signals at once, with any JSON-compatible value: strings, numbers, booleans, arrays, or objects.
Using @signals (Hyper)
The @signals directive creates signals from PHP data, which is more natural in Laravel. It provides several syntax options for different use cases.
Basic Array Syntax
Pass an associative array to create signals:
<div @signals(['username' => $user->name, 'role' => $user->role])>
<!-- Signals created from PHP variables -->
</div>Variable Syntax
Use PHP variables directly with special naming conventions:
<div @signals($username, $_editing, $userId_)>
<!-- Creates signals based on variable names -->
</div>Variable naming rules:
$variable→ Creates regular signalvariable$_variable→ Creates local signal_variable(not sent to server)$variable_→ Creates locked signalvariable_(protected from tampering)
Example:
<?php
$username = 'John';
$_showPassword = false;
$userId_ = auth()->id();
?>
<div @signals($username, $_showPassword, $userId_)>
<!-- Creates: username, _showPassword, userId_ -->
</div>Spread Syntax
Spread PHP arrays or objects to create multiple signals at once:
<div @signals(...$user)>
<!-- If $user = ['name' => 'John', 'email' => 'john@example.com'] -->
<!-- Creates signals: name, email -->
</div>You can spread Laravel models and collections:
<div @signals(...$contact)>
<!-- If $contact is an Eloquent model -->
<!-- All model attributes become signals -->
</div>Spread with type prefixes:
<div @signals(...$user_, ...$_ui)>
<!-- Spread $user_ as locked signals: userId_, role_, etc. -->
<!-- Spread $_ui as local signals: _theme, _collapsed, etc. -->
</div>Mixing Syntaxes
Combine different approaches in one directive:
<div @signals(
['name' => $user->name, 'email' => $user->email],
$_editing,
...$permissions
)>
<!-- Combines arrays, variables, and spread syntax -->
</div>Automatic Type Conversion
The @signals directive automatically converts Laravel types:
<div @signals($user)>
<!-- Eloquent Model → Converted via toArray() -->
</div>
<div @signals($contacts)>
<!-- Collection → Converted via toArray() -->
</div>
<div @signals($results)>
<!-- LengthAwarePaginator → Converted via toArray() -->
</div>
<div @signals($custom)>
<!-- JsonSerializable → Converted via jsonSerialize() -->
<!-- Arrayable → Converted via toArray() -->
</div>Benefits of @signals:
- Pass PHP variables directly without manual JSON encoding
- Automatic type conversion for Laravel objects
- Automatic escaping and proper JSON encoding
- Support for variable syntax, spreads, and arrays
- Required for creating locked signals (security feature)
Automatic Creation via data-bind (Datastar)
When you bind an input to a signal that doesn't exist, Datastar creates it automatically:
<input data-bind="email" placeholder="Enter email" />
<!-- Signal 'email' is created automatically if it doesn't exist -->The signal takes its initial value from the input's current value. This is convenient for forms where you don't need to declare signals upfront.
Using data-computed (Datastar)
Computed signals derive their values from other signals and update automatically:
<div data-signals="{firstName: 'John', lastName: 'Doe'}">
<div data-computed:names="$firstName + ' ' + $lastName"></div>
<p data-text="$names"></p>
<!-- Displays: John Doe -->
</div>Computed signals are read-only. When firstName or lastName changes, names updates automatically.
Signal Scope (Datastar)
Signals are globally scoped once created. A signal created anywhere on the page is accessible everywhere on that page:
<!-- Create signal in header -->
<header data-signals="{siteName: 'My App'}">
<h1 data-text="$siteName"></h1>
</header>
<!-- Use same signal in footer -->
<footer>
<p>© 2025 <span data-text="$siteName"></span></p>
</footer>Both locations display "My App" and update together if siteName changes. This eliminates prop drilling and makes state sharing simple.
Signal Naming
Datastar uses dot notation for nested signal paths:
<div data-signals="{user: {name: 'John', age: 30}}">
<p data-text="$user.name"></p> <!-- Access nested property -->
<button data-on:click="$user.age = $user.age + 1">Birthday</button>
</div>You can organize related signals into namespaces using objects, making your state structure clearer.
Reading Signals
In the Frontend (Datastar)
Use the $ prefix to read signals in any Datastar expression:
<div data-signals="{price: 100, quantity: 2}">
<!-- Display signal value -->
<p data-text="$price"></p>
<!-- Use in computation -->
<p data-text="'Total: $' + ($price * $quantity)"></p>
<!-- Use in conditional -->
<div data-show="$quantity > 0">Items in cart</div>
</div>Signals work in any Datastar expression: data-text, data-show, data-class, data-on:click, etc.
In the Backend (Hyper)
Use the signals() helper to read signals sent from the frontend:
public function updateCart()
{
// Get a specific signal with optional default
$quantity = signals('quantity', 1);
// Get a nested signal
$userName = signals('user.name');
// Check if a signal exists
if (signals()->has('coupon')) {
$discount = signals('coupon');
}
// Get all signals
$allSignals = signals()->all();
// Get as Collection for Laravel methods
$signals = signals()->collect();
// Get only specific signals
$subset = signals()->only(['name', 'email', 'age']);
}The signals() helper works like Laravel's request() helper, providing familiar methods for accessing data sent from the frontend.
Updating Signals
From the Frontend (Datastar)
Update signals directly in Datastar expressions:
<div data-signals="{count: 0}">
<button data-on:click="$count = $count + 1">Increment</button>
<button data-on:click="$count = $count - 1">Decrement</button>
<button data-on:click="$count = 0">Reset</button>
<span data-text="$count"></span>
</div>You can use any JavaScript-like expression for updates:
<button data-on:click="$count++">Increment</button>
<button data-on:click="$count += 5">Add 5</button>From the Backend (Hyper)
Use hyper()->signals() to update signals from your Laravel controller:
public function increment()
{
$count = signals('count', 0);
$newCount = $count + 1;
return hyper()->signals(['count' => $newCount]);
}You can update multiple signals at once:
return hyper()->signals([
'count' => 5,
'message' => 'Updated!',
'errors' => []
]);Creating signals on update:
If you update a signal that doesn't exist, it will be created automatically:
// Even if 'newSignal' doesn't exist on the frontend yet
return hyper()->signals(['newSignal' => 'Hello']);
// Datastar will create it automaticallyAlternative single signal syntax:
// Update a single signal using key/value syntax
return hyper()->signals('count', 5);Deleting Signals
Signals can be deleted from both the frontend and backend.
From the Frontend (Datastar)
Datastar deletes signals by setting their value to null:
<div data-signals="{user: 'John', temp: 'data'}">
<!-- Delete the temp signal -->
<button data-on:click="$temp = null">Remove Temp</button>
<div data-text="$temp"></div>When a signal is set to null, Datastar removes it from the signal store entirely.
From the Backend (Hyper)
Use hyper()->forget() to delete signals from your controller:
// Forget a single signal
return hyper()->forget('temp');
// Forget multiple signals
return hyper()->forget(['temp', 'cache', 'draft']);
// Forget all signals
return hyper()->forget();The forget() method is chainable with other operations:
return hyper()
->forget(['temp', 'draft'])
->signals(['message' => 'Cleaned up!']);How it works:
Internally, forget() sets the specified signals to null, which tells Datastar to remove them:
// These are equivalent
return hyper()->forget('temp');
return hyper()->signals(['temp' => null]);Forgetting locked signals:
When you forget a locked signal, Hyper also clears it from the session storage:
return hyper()->forget('userId_'); // Also clears from sessionSignal Types
Hyper and Datastar support three types of signals, each with different transmission behavior.
Regular Signals (Datastar)
Regular signals are sent to the server with every request:
<div data-signals="{username: 'John', email: 'john@example.com'}">
<button data-on:click="@postx('/update')">Save</button>
</div>When the button is clicked, both username and email are automatically included in the request payload. Your Laravel controller receives them via signals('username') and signals('email').
Use regular signals for:
- Form data that needs validation
- User input that affects server logic
- State the server needs to process
Local Signals (Datastar)
Local signals (prefixed with _) stay in the browser and are never sent to the server:
<div data-signals="{
username: 'John',
_showPassword: false,
_accordionOpen: false
}">
<!-- username is sent to server, _showPassword and _accordionOpen stay local -->
</div>When you make a request, only username is sent. The local signals remain in the browser, reducing payload size and network traffic.
Use local signals for:
- UI state (dropdowns open/closed, tabs selected)
- Temporary display toggles
- Client-side-only interactions
- Loading and error states for specific UI elements
Important: Even though local signals aren't sent to the server, the server can still update them using hyper()->signals(['_showPassword' => true]). The underscore prefix only controls frontend-to-server transmission, not server-to-frontend updates.
Locked Signals (Hyper Extension)
Locked signals (suffixed with _) are sent to the server and protected from tampering. They can only be created using the @signals directive, not data-signals:
<div @signals(['userId_' => auth()->id(), 'role_' => auth()->user()->role])>
<!-- These signals are protected from client-side modification -->
</div>When you create locked signals with @signals, Hyper:
- Stores their values securely in the encrypted session
- Validates on every request that values haven't been tampered with
- Throws
HyperSignalTamperedExceptionif tampering is detected
Trying to create locked signals with data-signals won't provide this protection:
<!-- ❌ This doesn't create protected locked signals -->
<div data-signals="{userId_: 123}">The security mechanism requires PHP-side processing through the @signals directive to store and validate the values.
Use locked signals for:
- User IDs and authentication data
- Permission levels and roles
- Pricing information
- Any sensitive data that should only change through authorized server actions
Learn more in Advanced: Locked Signals.
Validation
Hyper integrates Laravel's validation system with signals, making validation feel natural.
Validating Signals
Use signals()->validate() exactly like request()->validate():
public function store()
{
$validated = signals()->validate([
'title' => 'required|string|max:255',
'email' => 'required|email',
'age' => 'integer|min:18'
]);
// Use validated data...
}You can also provide custom messages and attribute names:
$validated = signals()->validate(
['email' => 'required|email|unique:users'],
['email.unique' => 'This :attribute is already taken.'],
['email' => 'email address']
);If validation fails, Hyper automatically:
- Creates an
errorssignal with Laravel's error structure - Returns it to the frontend
- Displays errors using
data-errorattributes
Displaying Errors
Use the data-error attribute (Hyper extension) to display validation errors:
<div data-signals="{title: '', errors: []}">
<input data-bind="title" />
<div data-error="title"></div>
</div>When validation fails, data-error="title" automatically shows the first error message for the title field. When validation passes or you clear errors, the element becomes hidden.
Learn more in Essentials: Validation.
Common Patterns
Form State Management
<form @signals(['name' => '', 'email' => '', 'errors' => []])
data-on:submit__prevent="@postx('/contacts')">
<input data-bind="name" />
<div data-error="name"></div>
<input data-bind="email" type="email" />
<div data-error="email"></div>
<button type="submit">Save</button>
</form>public function store()
{
$validated = signals()->validate([
'name' => 'required|string|max:255',
'email' => 'required|email'
]);
Contact::create($validated);
return hyper()->signals([
'name' => '',
'email' => '',
]);
}Toggle States
<div data-signals="{_showDetails: false}">
<button data-on:click="$_showDetails = !$_showDetails">
<span data-text="$_showDetails ? 'Hide' : 'Show'"></span> Details
</button>
<div data-show="$_showDetails">
<p>Detailed information here...</p>
</div>
</div>Counters and Metrics
<div @signals(['downloads' => 0, 'limit' => 100])>
<p data-text="$downloads + ' / ' + $limit + ' downloads'"></p>
<button data-on:click="@postx('/download')"
data-attr:disabled="$downloads >= $limit">
Download
</button>
</div>public function download()
{
$downloads = signals('downloads', 0);
if ($downloads >= signals('limit', 100)) {
return hyper()->signals(['error' => 'Download limit reached']);
}
// Process download...
return hyper()->signals(['downloads' => $downloads + 1]);
}Master-Detail State
<div @signals(['selectedId' => null])>
<ul>
@foreach($users as $user)
<li data-on:click="$selectedId = {{ $user->id }}"
data-class:active="$selectedId === {{ $user->id }}">
{{ $user->name }}
</li>
@endforeach
</ul>
<div data-show="$selectedId !== null">
<button data-on:click="@getx('/users/' + $selectedId)">
Load Details
</button>
</div>
</div>Signal Lifecycle
Understanding how signals flow through a request helps you use them effectively:
- Initialization: Signals are created on page load via
data-signalsor@signals - Frontend Updates: User interactions modify signal values through Datastar expressions
- Request: When an HTTP action fires (e.g.,
@postx), Datastar sends all regular signals (not local_signals) to the server - Server Processing: Your Laravel controller reads signals using
signals(), processes logic, runs validation - Response: Server returns updated signals via
hyper()->signals() - Frontend Update: Datastar receives signal updates and automatically updates all dependent elements
This cycle creates a reactive loop where the server and frontend stay synchronized, with the server remaining the source of truth for business logic and validation.
Debugging Signals
View Current Signal Values
Add a debug panel to see all current signals:
<div data-signals="{count: 0, user: 'John'}">
<!-- Your app content -->
@if(config('app.debug'))
<pre data-text="JSON.stringify($$signals, null, 2)"></pre>
@endif
</div>The $$signals (double dollar) accesses Datastar's internal signal store.
Log Signal Changes
Use data-effect to log when signals change:
<div data-signals="{count: 0}">
<div data-effect="console.log('Count changed:', $count)"></div>
<button data-on:click="$count++">Increment</button>
</div>Every time count changes, the console logs the new value.
Server-Side Inspection
Log signals received by your controller:
public function update()
{
\Log::info('Received signals:', signals()->all());
// Your logic...
}What's Next?
Now that you understand signals, explore related topics:
- Blade Integration: Master
@signals,@fragment, and other directives - Actions: Learn how to trigger server requests that send signals
- Responses: Explore the full power of the
hyper()response builder - Validation: Deep dive into validating signals with Laravel
- Forms: File Uploads: Handle file uploads with signals
- Advanced: Locked Signals: Security and tampering protection

