livewire/livewire: Fix server-side JS evaluation on mount running before the component is registered
Contributed by lazerg
Part of the livewire v4.4.7 release
Fix — livewire/livewire 4.x (PR #10494, fixes #10493)
If a component calls $this->js() during mount(), that JavaScript is serialized into the initial page payload as a mount-time effect and executed in the browser during hydration. On the 4.x branch prior to this PR, those initial effects were processed from the Component constructor — a moment before the component had been registered in Livewire's client-side store. Any $wire call inside that JavaScript therefore couldn't resolve its owning component, the $wire proxy caught the error, and the action silently never fired. PR #10494 moves initial effect processing to just after component registration, so mount-time $wire calls resolve and run the way the code reads. For anyone upgrading from v3.8.10 into the 4.x line, it's a small internal change that removes a genuinely nasty silent-failure mode.
What changed
The diff is small and entirely internal:
- In
js/component.js, theComponentconstructor no longer callsthis.processEffects(this.effects). The comment that justified it — "Effects will be processed after every request, but we'll also handle them on initialization" — was right about intent but wrong about placement. - In
js/store.js,initComponent()now processes initial effects immediately after storing the component:
components[component.id] = component
// Process initial effects after registering so mount-time effects like `$this->js()` can resolve `$wire`...
component.processEffects(component.effects)
- A new browser test was added to
SupportJsEvaluation/BrowserTest.phpthat locks the behavior in: a component whosemount()calls$this->js('$wire.increment()')must render with the count already incremented.
The bug in practice
Consider the exact pattern the new test covers:
class Counter extends Component
{
public $count = 0;
public function mount()
{
$this->js('$wire.increment()');
}
public function increment()
{
$this->count++;
}
public function render()
{
return <<<'HTML'
<div>
<span dusk="count">{{ $count }}</span>
</div>
HTML;
}
}
Reading this, you'd expect the page to load showing 1: mount() queues JavaScript that invokes the increment action, and that action bumps the count. Before the fix, it showed 0 — silently. The JavaScript shipped to the browser fine, but when it executed, $wire.increment() couldn't find the component because initComponent() hadn't yet stored it in the registry. Rather than throwing, the $wire proxy caught the resolution failure and returned a no-op. No error, no warning — just an action that never happens, which is the hardest kind of bug to track down.
Note the scope: only mount-time JavaScript that reaches back into the component via $wire was affected. Plain side effects in mount() — console.log(), setting window globals, dispatching browser events — already worked, because they never need to resolve the component at all.
Why the ordering matters
Effects are the transport for much of Livewire's server-to-client communication: dispatched events, redirects, and — for the JavaScript Evaluation feature — strings of JS to evaluate. Every request response ends with effects that get processed after the component is updated and registered. The initialization path was the outlier: it processed effects during construction, one step too early in the registration sequence. Moving the call into initComponent() puts page load on the same footing as every subsequent request — register first, process effects second.
Upgrade impact
- No API changes. The fix is purely about when initial effects run. If you never used
$this->js()insidemount(), you'll notice nothing. - Broken patterns now work as written. Code like the
Counterabove starts behaving the way it reads — no changes required on your side. - Workarounds can be removed. If you dodged this bug by moving
js()calls out ofmount(), deferring them withsetTimeout(...)inside the JS string, or re-triggering them from a lifecycle hook, you can simplify back to the direct form. - Double-check timing-sensitive code. If you somehow depended on the old ordering — for example, you already compensated for mount-time actions not firing — verify the new behavior: actions queued in
mount()now actually execute on load.
Takeaways
$this->js()calls made inmount()ship as initial effects and now run after the component is registered, so$wirecalls inside them resolve correctly.- Previously,
$wireactions in mount-time JS silently no-op'd; the fix eliminates that silent failure mode. - The change is internal — no public API, config, or template changes are needed to upgrade.
- A regression browser test (
$this->js('$wire.increment()')inmount()) is included to keep this fixed.