# Inertia Vue Development

> Develops Inertia.js v3 Vue client-side applications. Activates when creating Vue pages, forms, or navigation; using <Link>, <Form>, useForm, useHttp, useLayoutProps, or router; working with deferred props, prefetching, optimistic updates, instant visits, or polling; or when user mentions Vue with Inertia, Vue pages, Vue forms, or Vue navigation.

- Skill: `imsus/inertia-vue-development` (Agent Skill)
- Install (CLI): `npx skillmds@latest add imsus/inertia-vue-development`
- Raw SKILL.md: https://api.skillmd.com/api/skills/imsus/inertia-vue-development/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: MIT
- Author: imsus (https://skillmd.com/u/imsus)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/imsus/inertia-vue-development

---


# Inertia Vue Development

## When to Apply

Activate this skill when:

- Creating or modifying Vue page components for Inertia
- Working with forms in Vue (using `<Form>`, `useForm`, or `useHttp`)
- Implementing client-side navigation with `<Link>` or `router`
- Using v3 features: deferred props, prefetching, optimistic updates, instant visits, layout props, HTTP requests, WhenVisible, InfiniteScroll, once props, flash data, or polling
- Building Vue-specific features with the Inertia protocol

## Documentation

Use `search-docs` for detailed Inertia v3 Vue patterns and documentation.

## Basic Usage

### Page Components Location

Vue page components should be placed in the `resources/js/pages` directory.

### Page Component Structure

<!-- Basic Vue Page Component -->

```vue
<script setup>
defineProps({
  users: Array,
});
</script>

<template>
  <div>
    <h1>Users</h1>
    <ul>
      <li v-for="user in users" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
  </div>
</template>
```

## Client-Side Navigation

### Basic Link Component

Use `<Link>` for client-side navigation instead of traditional `<a>` tags:

<!-- Inertia Vue Navigation -->

```vue
<script setup>
import { Link } from "@inertiajs/vue3";
</script>

<template>
  <div>
    <Link href="/">Home</Link>
    <Link href="/users">Users</Link>
    <Link :href="`/users/${user.id}`">View User</Link>
  </div>
</template>
```

### Link with Method

<!-- Link with POST Method -->

```vue
<script setup>
import { Link } from "@inertiajs/vue3";
</script>

<template>
  <Link href="/logout" method="post" as="button"> Logout </Link>
</template>
```

### Prefetching

Prefetch pages to improve perceived performance:

<!-- Prefetch on Hover -->

```vue
<script setup>
import { Link } from "@inertiajs/vue3";
</script>

<template>
  <Link href="/users" prefetch> Users </Link>
</template>
```

### Programmatic Navigation

<!-- Router Visit -->

```vue
<script setup>
import { router } from "@inertiajs/vue3";

function handleClick() {
  router.visit("/users");
}

// Or with options
function createUser() {
  router.visit("/users", {
    method: "post",
    data: { name: "John" },
    onSuccess: () => console.log("Done"),
  });
}
</script>

<template>
  <Link href="/users">Users</Link>
  <Link href="/logout" method="post" as="button">Logout</Link>
</template>
```

## Form Handling

### Form Component (Recommended)

The recommended way to build forms is with the `<Form>` component:

<!-- Form Component Example -->

```vue
<script setup>
import { Form } from "@inertiajs/vue3";
</script>

<template>
  <Form action="/users" method="post" #default="{ errors, processing, wasSuccessful }">
    <input type="text" name="name" />
    <div v-if="errors.name">{{ errors.name }}</div>

    <input type="email" name="email" />
    <div v-if="errors.email">{{ errors.email }}</div>

    <button type="submit" :disabled="processing">
      {{ processing ? "Creating..." : "Create User" }}
    </button>

    <div v-if="wasSuccessful">User created!</div>
  </Form>
</template>
```

### Form Component With All Props

<!-- Form Component Full Example -->

```vue
<script setup>
import { Form } from "@inertiajs/vue3";
</script>

<template>
  <Form
    action="/users"
    method="post"
    #default="{
      errors,
      hasErrors,
      processing,
      progress,
      wasSuccessful,
      recentlySuccessful,
      setError,
      clearErrors,
      resetAndClearErrors,
      defaults,
      isDirty,
      reset,
      submit,
    }"
  >
    <input type="text" name="name" :value="defaults.name" />
    <div v-if="errors.name">{{ errors.name }}</div>

    <button type="submit" :disabled="processing">
      {{ processing ? "Saving..." : "Save" }}
    </button>

    <progress v-if="progress" :value="progress.percentage" max="100">
      {{ progress.percentage }}%
    </progress>

    <div v-if="wasSuccessful">Saved!</div>
  </Form>
</template>
```

### Form Component Reset Props

The `<Form>` component supports automatic resetting:

- `resetOnError` - Reset form data when the request fails
- `resetOnSuccess` - Reset form data when the request succeeds
- `setDefaultsOnSuccess` - Update default values on success

Use the `search-docs` tool with a query of `form component resetting` for detailed guidance.

<!-- Form with Reset Props -->

```vue
<script setup>
import { Form } from "@inertiajs/vue3";
</script>

<template>
  <Form
    action="/users"
    method="post"
    reset-on-success
    set-defaults-on-success
    #default="{ errors, processing, wasSuccessful }"
  >
    <input type="text" name="name" />
    <div v-if="errors.name">{{ errors.name }}</div>

    <button type="submit" :disabled="processing">Submit</button>
  </Form>
</template>
```

Forms can also be built using the `useForm` composable for more programmatic control. Use the `search-docs` tool with a query of `useForm helper` for guidance.

### `useForm` Composable

For more programmatic control or to follow existing conventions, use the `useForm` composable:

<!-- useForm Composable Example -->

```vue
<script setup>
import { useForm } from "@inertiajs/vue3";

const form = useForm({
  name: "",
  email: "",
  password: "",
});

function submit() {
  form.post("/users", {
    onSuccess: () => form.reset("password"),
  });
}
</script>

<template>
  <form @submit.prevent="submit">
    <input type="text" v-model="form.name" />
    <div v-if="form.errors.name">{{ form.errors.name }}</div>

    <input type="email" v-model="form.email" />
    <div v-if="form.errors.email">{{ form.errors.email }}</div>

    <input type="password" v-model="form.password" />
    <div v-if="form.errors.password">{{ form.errors.password }}</div>

    <button type="submit" :disabled="form.processing">Create User</button>
  </form>
</template>
```

## Inertia v3 Features

### HTTP Requests

Use the `useHttp` hook for standalone HTTP requests that do not trigger Inertia page visits. It provides the same developer experience as `useForm`, but for plain JSON endpoints.

<!-- useHttp Example -->

```vue
<script setup>
import { useHttp } from "@inertiajs/vue3";

const http = useHttp({
  query: "",
});

function search() {
  http.get("/api/search", {
    onSuccess: (response) => {
      console.log(response);
    },
  });
}
</script>

<template>
  <input v-model="http.query" @input="search" />
  <div v-if="http.processing">Searching...</div>
</template>
```

### Optimistic Updates

Apply data changes instantly before the server responds, with automatic rollback on failure:

<!-- Optimistic Update with Router -->

```vue
<script setup>
import { router } from "@inertiajs/vue3";

function like(post) {
  router
    .optimistic((props) => ({
      post: {
        ...props.post,
        likes: props.post.likes + 1,
      },
    }))
    .post(`/posts/${post.id}/like`);
}
</script>
```

Optimistic updates also work with `useForm` and the `<Form>` component:

<!-- Optimistic Update with Form Component -->

```vue
<template>
  <Form
    action="/todos"
    method="post"
    :optimistic="
      (props, data) => ({
        todos: [...props.todos, { id: Date.now(), name: data.name, done: false }],
      })
    "
  >
    <input type="text" name="name" />
    <button type="submit">Add Todo</button>
  </Form>
</template>
```

### Instant Visits

Navigate to a new page immediately without waiting for the server response. The target component renders right away with shared props, while page-specific props load in the background.

<!-- Instant Visit with Link -->

```vue
<script setup>
import { Link } from "@inertiajs/vue3";
</script>

<template>
  <Link href="/dashboard" component="Dashboard">Dashboard</Link>

  <Link href="/posts/1" component="Posts/Show" :page-props="{ post: { id: 1, title: 'My Post' } }">
    View Post
  </Link>
</template>
```

### Layout Props

Share dynamic data between pages and persistent layouts:

<!-- Layout Props in Layout -->

```vue
<script setup>
import { useLayoutProps } from "@inertiajs/vue3";

const layout = useLayoutProps({
  title: "My App",
  showSidebar: true,
});
</script>

<template>
  <header>{{ layout.title }}</header>
  <aside v-if="layout.showSidebar">Sidebar</aside>
  <main>
    <slot />
  </main>
</template>
```

<!-- Setting Layout Props from Page -->

```vue
<script setup>
import { setLayoutProps } from "@inertiajs/vue3";

setLayoutProps({
  title: "Dashboard",
  showSidebar: false,
});
</script>

<template>
  <h1>Dashboard</h1>
</template>
```

### Deferred Props

Use deferred props to load data after initial page render:

<!-- Deferred Props with Empty State -->

```vue
<script setup>
defineProps({
  users: Array,
});
</script>

<template>
  <div>
    <h1>Users</h1>
    <div v-if="!users" class="animate-pulse">
      <div class="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
      <div class="h-4 bg-gray-200 rounded w-1/2"></div>
    </div>
    <ul v-else>
      <li v-for="user in users" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
  </div>
</template>
```

### Polling

Automatically refresh data at intervals:

<!-- Polling Example -->

```vue
<script setup>
import { router } from "@inertiajs/vue3";
import { onMounted, onUnmounted } from "vue";

defineProps({
  stats: Object,
});

let interval;

onMounted(() => {
  interval = setInterval(() => {
    router.reload({ only: ["stats"] });
  }, 5000);
});

onUnmounted(() => {
  clearInterval(interval);
});
</script>

<template>
  <div>
    <h1>Dashboard</h1>
    <div>Active Users: {{ stats.activeUsers }}</div>
  </div>
</template>
```

### WhenVisible

Lazy-load a prop when an element scrolls into view. Useful for deferring expensive data that sits below the fold:

<!-- WhenVisible Example -->

```vue
<script setup>
import { WhenVisible } from "@inertiajs/vue3";

defineProps({
  stats: Object,
});
</script>

<template>
  <div>
    <h1>Dashboard</h1>

    <WhenVisible data="stats" :buffer="200">
      <template #fallback>
        <div class="animate-pulse">Loading stats...</div>
      </template>

      <template #default="{ fetching }">
        <div>
          <p>Total Users: {{ stats.total_users }}</p>
          <p>Revenue: {{ stats.revenue }}</p>
          <span v-if="fetching">Refreshing...</span>
        </div>
      </template>
    </WhenVisible>
  </div>
</template>
```

## Server-Side Patterns

Server-side patterns (Inertia::render, props, middleware) are covered in inertia-laravel guidelines.

## Common Pitfalls

- Using traditional `<a>` links instead of Inertia's `<Link>` component (breaks SPA behavior)
- Forgetting that Vue components must have a single root element
- Forgetting to add loading states (skeleton screens) when using deferred props
- Not handling the `undefined` state of deferred props before data loads
- Using `<form>` without preventing default submission (use `<Form>` component or `@submit.prevent`)
- Forgetting to check if `<Form>` component is available in your Inertia version
- Using `router.cancel()` instead of `router.cancelAll()` (v3 breaking change)
- Using `router.on('invalid', ...)` or `router.on('exception', ...)` instead of the renamed `httpException` and `networkError` events

