skip to content

In an Inertia app, what does <Link> do that a plain anchor does not, and how do you make it send a DELETE?

level: juniorimportance: must knowfreq 58%

answer

  1. intercepts the click, no document reload
  2. XHR visit, then pushState
  3. method="delete" on the Link
  4. non-GET links render a button
  5. modifier clicks go to the browser

basics

~10 s
<Link> intercepts the click and makes an Inertia visit, an XHR that returns a page object, then swaps the component and pushes history. method="delete" sends a DELETE, and non-GET links render as a <button>.

solid answer

~50 s

`<Link href="/invoices">` renders an anchor, but on a plain left click it calls the Inertia router instead of letting the browser load a new document: an XHR with `X-Inertia: true`, a JSON page object back, a component swap and a `pushState` history entry, so layouts, state and the loaded bundle stay. Ctrl, Cmd, Shift or Alt clicks, middle clicks and `target="_blank"` are left to the browser. `method="delete"` (or `post`, `put`, `patch`) turns it into a mutation, `data` adds a body, and for any non-GET method the React adapter renders a `<button type="button">` instead of an `<a>`, because "open in new tab" on a DELETE link makes no sense. Non-GET links also default `preserveState` to `true`. The server usually answers with a redirect, which the Laravel adapter turns into a `303` so the follow-up request is a `GET`.

code

jsx · 21 lines
jsx
import { Link } from '@inertiajs/react'

export function InvoiceRow({ invoice }) {
  return (
    <tr>
      <td>
        <Link href={`/invoices/${invoice.id}`}>{invoice.number}</Link>
      </td>
      <td>
        <Link
          href={`/invoices/${invoice.id}`}
          method="delete"
          data={{ reason: 'duplicate' }}
          preserveScroll
        >
          Void
        </Link>
      </td>
    </tr>
  )
}

go deeper

for a junior

Recall that <Link> makes an XHR visit instead of a full reload, and that method, data and replace customise it.

for a middle

Explain the intercept rules, why non-GET links render buttons and preserve state, and how the server answers with a redirect.

for a senior

Choose between <Link>, plain anchors and server-side Inertia::location for downloads, external sites and non-Inertia pages.

for a principal

Set conventions for navigation and mutations, such as typed route helpers and buttons for actions, so accessibility and behaviour stay consistent.

## What `<Link>` replaces In an Inertia app, pages are React, Vue or Svelte components and routing stays in Laravel. A plain `<a href="/invoices">` would still work, but every click would download a new HTML document, re-boot the JavaScript bundle and throw away layout state. The `<Link>` component from `@inertiajs/react` (or the Vue and Svelte adapters) keeps the single-page feel. ## What happens on click 1. `<Link>` renders a normal element, an `<a>` by default, so the URL is visible, crawlable and copyable. 2. On a plain primary-button click it prevents the default navigation and calls the Inertia router. 3. The router sends an XHR with `X-Inertia: true` and the current asset version. 4. Laravel's controller returns `Inertia::render(...)`; for this request that is the bare page object as JSON. 5. The client swaps the page component, keeps persistent layouts mounted, pushes a history entry and resets scroll unless told otherwise. The click is **not** intercepted when the user holds Ctrl, Cmd, Shift or Alt, uses a non-primary button, or the anchor has a `target` other than `_self`; the browser handles those as ordinary links, which keeps "open in new tab" working. ## Mutations from a link | Prop | Effect | |---|---| | `method="post"`, `"put"`, `"patch"`, `"delete"` | the visit uses that HTTP method; default is `get` | | `data={{ reason: 'duplicate' }}` | request body for non-GET; merged into the query string for GET | | `as="button"` | render another element; non-GET links render a `button` regardless | | `headers={{ ... }}` | extra request headers; Inertia's own headers cannot be overridden | | `replace` | replace the current history entry instead of pushing a new one | | `preserveState` | keep the page component's local state; defaults to `true` for non-GET links | The React adapter computes the element as `button` whenever the method is not `get`, and gives it `type="button"`. The docs discourage anchors that issue `POST`, `PUT`, `PATCH` or `DELETE`, because "open in new tab" would issue a `GET` to a URL meant for a mutation. ## What the server should return A link that deletes an invoice should hit a route such as `Route::delete('/invoices/{invoice}', ...)` whose action deletes and redirects, for example back to the list. Two adapter behaviours make that work: - The Laravel adapter changes a `302` answer to a `PUT`, `PATCH` or `DELETE` Inertia request into `303 See Other`, so the browser follows it with a `GET`. - The follow-up `GET` returns the list page's page object, and because non-GET links preserve state by default, the page keeps its local state such as an open filter panel. CSRF is handled for you: the client sends the `X-XSRF-TOKEN` header read from Laravel's `XSRF-TOKEN` cookie. ## Common mistakes - **Using `<Link>` for a file download.** The response is a file, not a page object, so the client treats it as an error; link to downloads with a plain anchor. - **Putting a `DELETE` on a GET route.** A link with `method="delete"` still needs a `Route::delete` on the server, or Laravel answers `405`. - **Adding `onClick` navigation to a `<div>`.** Use `<Link>` or `router.visit()`; a clickable `div` loses keyboard access and the new-tab behaviour. - **Forgetting `preserveScroll` on in-page actions.** A "void" link in the middle of a long table scrolls the user to the top after the redirect. ## When not to use `<Link>` - **External sites.** Use a plain `<a href>`; an XHR visit cannot navigate to another origin. - **Downloads.** A file response is not a page object; use a plain anchor to the download route. - **Areas outside the Inertia app**, such as a Blade-rendered page. Use a plain anchor, or `Inertia::location()` from the server when the decision is made there. ## Wayfinder and route helpers The React and Vue starter kits include Laravel Wayfinder, which generates typed functions for controller actions. Passing such an object as `href` lets `<Link>` infer both the URL and the HTTP method, so a `destroy(invoice.id)` object produces a DELETE link without a separate `method` prop. ## Active states `usePage()` exposes the current `url` and `component`, so navigation can mark the active link with a comparison such as `url.startsWith('/invoices')`. While a link's request is in flight, the element carries a `data-loading` attribute that CSS can style.

  • Why does a DELETE <Link> render as a button rather than an anchor?
    An anchor promises a URL you can open, bookmark or open in a new tab, which the browser does with a `GET`. For a mutation that would hit the wrong route or do nothing useful. The React adapter therefore renders any non-GET `<Link>` as `<button type="button">`, which is also the right element for assistive technology to announce as an action.
  • Which clicks on a <Link> are not intercepted by Inertia?
    Clicks with Ctrl, Cmd, Shift or Alt held, clicks with a non-primary mouse button, and clicks on anchors whose `target` is set to something other than `_self`. Those fall through to the browser, so opening a page in a new tab or window loads it as a normal document request.

saying these in an interview costs you the question

  • <Link> fetches the new page's HTML and replaces the body.
  • A DELETE link should be an <a> so it can open in a new tab.
  • Inertia links reload the JavaScript bundle on every click.
  • <Link> can navigate to an external site with an XHR visit.
  • The method prop only accepts get and post.