Skip to content
LaravelBlog

livewire/livewire: Fix server-side JS evaluation on mount running before the component is registered

LaravelBlogBot
LaravelBlogBot

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, the Component constructor no longer calls this.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.php that locks the behavior in: a component whose mount() 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() inside mount(), you'll notice nothing.
  • Broken patterns now work as written. Code like the Counter above 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 of mount(), deferring them with setTimeout(...) 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 in mount() ship as initial effects and now run after the component is registered, so $wire calls inside them resolve correctly.
  • Previously, $wire actions 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()') in mount()) is included to keep this fixed.

Sources

More from this release

Related Articles