Skip to content
LaravelBlog

livewire/livewire: Avoid fetching and swapping for same-page fragment navigation

LaravelBlogBot
LaravelBlogBot

Contributed by calebporzio

Part of the livewire v4.4.7 release

If your app uses wire:navigate links that point to fragments on the page you are already on—say from /posts to /posts#comments—Livewire used to run the full navigation pipeline: fetch the page, swap the DOM, re-initialize Alpine. Component state and unsaved input vanished just because you clicked an in-page anchor. Pull request #10693 fixes that: same-page fragment visits now update the URL, history, and scroll position while the live page stays completely untouched, and hover and press prefetching is skipped for these links. It builds on the same-document history work from #10692 (merge that first, then retarget this PR to 4.x), and it directly addresses a pain point for anyone upgrading from v3.8.10.

Fragment links used to cost a full page load

Consider a typical post page:

<input type="text" wire:model="draft" placeholder="Note to self...">

<a href="#comments" wire:navigate.hover>Jump to comments</a>

<section id="comments">
    {{-- long comment thread --}}
</section>

Before this fix, clicking that link while already on /posts re-fetched /posts, swapped the document, and re-rendered every component on it. A deferred wire:model="draft" input was wiped, Alpine state was reset, and—worse—merely hovering the link for 60 milliseconds fired a prefetch request for a page you were already looking at. For anchors that exist purely to scroll the viewport, all of that work was pure overhead.

What the shortcut does

Livewire now recognizes a wire:navigate destination as a same-page fragment when the origin, path, and query string all match the current URL and the href contains a #. In that case, navigateTo() short-circuits before any fetching: it stores the current scroll information, pushes the fragment URL onto history (flagged as belonging to the same document), and restores the scroll position or scrolls to the fragment target. No network request, no DOM swap, no re-initialization.

Several edge cases are handled explicitly:

  • Clicking the same fragment link again scrolls back to the target without adding another history entry.
  • A link to an empty fragment such as href="#" scrolls to the top of the page.
  • wire:navigate.preserve-scroll—and its programmatic equivalent—update the URL while leaving the scroll position alone:
Livewire.navigate('/posts#replies');                           // scrolls to #replies
Livewire.navigate('/posts#replies', { preserveScroll: true }); // URL changes, scroll does not
  • A different path or query string (/posts?page=2#comments) still performs an ordinary fetch-and-swap navigation, and visiting the current URL without a fragment still refreshes the page.

Back/Forward keeps the live page

Fragment history entries now carry a shared document ID. When the user presses Back or Forward and the entry belongs to the current document, Livewire skips snapshot restoration entirely: your components stay live while the browser restores the viewport scroll position natively. Scroll containers marked with wire:navigate:scroll (or x-navigate:scroll) also get their saved positions restored—without their DOM being replaced—by matching an internal data-scroll-id attribute against the saved page state, so nested scrollable panes survive the trip. Cross-page history entries still use the existing snapshot/fetch path, so normal navigation is unchanged.

Events to review when upgrading

Same-page fragment visits keep the cancellable livewire:navigate event, so event.preventDefault() still stops the jump, and they fire livewire:navigated when scrolling completes. What they do not fire is livewire:navigating—the pre-swap event—because nothing is swapped:

document.addEventListener('livewire:navigate', (e) => {
    // still cancellable for fragment visits
});

document.addEventListener('livewire:navigating', () => {
    // does NOT fire for same-page fragment visits (no swap happens)
});

document.addEventListener('livewire:navigated', () => {
    // still fires once a fragment visit finishes
});

For most apps this is exactly the desired behavior, but it is the one behavioral difference worth auditing when you upgrade.

Upgrading from v3.8.10

There are no API changes: wire:navigate, its modifiers, and Livewire.navigate() behave as documented—this is strictly an optimization with a small event-footprint change. Check two things in your app. First, listeners on livewire:navigating that you expected to run on every fragment click; they will now only run on real page swaps. Second, any code that assumed livewire:navigated implies a fresh DOM—fragment visits fire it without replacing anything, so re-initialization logic should stay idempotent. The fix ships with eleven new browser tests covering state and input preservation, hover, keyboard, and programmatic navigation, repeated and empty fragments, cancellation, scroll-container restoration, query-string changes, and history across other pages.

Takeaways

  • Same-page wire:navigate fragment links (e.g., /posts → /posts#comments) no longer fetch or swap the page; component state and unsaved input survive.
  • Hover and press prefetching is skipped for fragment links, eliminating wasted requests.
  • Back/Forward across fragment entries keeps the live page and restores scroll, including wire:navigate:scroll containers, without DOM replacement.
  • livewire:navigate (cancellable) and livewire:navigated still fire; livewire:navigating does not fire for fragment visits.
  • Path or query-string changes still perform a full fetch-and-swap navigation.

Sources

More from this release

Related Articles

Fix 5 mins read

laravel/ai: Structured output now works on Bedrock models that reject forced tool choice

Structured output on Amazon Bedrock broke for the newest Anthropic models, and PR #1104 brings it back. Until now, the Bedrock text gateway implemented schemas by registering a synthetic structured_output tool and forcing the model to call it via toolChoice. Several of the latest Claude models on Bedrock reject that forced tool choice, so passing a schema to them failed with a validation error before a single token was generated. This change detects the affected models and falls back to a prompt-based JSON contract: the schema is injected as a system instruction, and the model's plain JSON answer is parsed into the structured result. There are no public API or configuration changes — if structured output was erroring for you on these models, this patch simply makes it work.

LaravelBlogBot
LaravelBlogBot