Not every request should navigate to a new page.
A search box may call a JSON endpoint. A dashboard may refresh one widget. A settings panel may validate fields as the user types. A file uploader may report progress without replacing the current screen.
Inertia 3.x gives these interactions a dedicated tool: useHttp.
The new hook brings the familiar experience of useForm to standalone HTTP requests. It tracks processing, validation errors, upload progress, success states, and cancellation. It also supports optimistic updates and Laravel Precognition.
For teams using Laravel as a PHP web framework, this closes a practical gap. You can keep Inertia for page navigation and use plain JSON requests for everything else.

useHttp: Requests without page navigation
An Inertia visit updates page props and participates in the Inertia page lifecycle. That is useful when the URL and page state should change.
useHttp takes a different path. It sends a regular HTTP request to a JSON endpoint. It does not navigate, change browser history, or process an Inertia page response.
That makes it a natural fit for:
- Search and autocomplete
- Actions inside a dashboard
- External API calls
- JSON endpoints
- Background mutations
- Inline editing
- File uploads
- Live validation
The basic Vue setup is small:
<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>
Your Laravel endpoint should return JSON rather than an Inertia response:
Route::get('/api/search', function (Request $request) {
return response()->json([
'results' => Product::query()
->where('name', 'like', "%{$request->query('query')}%")
->limit(10)
->get(),
]);
});
The hook provides convenience methods for get, post, put, patch, and delete. You can also use submit when the HTTP method needs to be dynamic.
Each method returns a promise containing the parsed JSON response. TypeScript users can also type HTTP request and response data.
Reactive state for every request
useHttp exposes the state you usually need to build a complete interface:
processingerrorshasErrorsprogresswasSuccessfulrecentlySuccessfulisDirty
This means request state stays close to the data and the component using it. You do not need separate loading flags, error objects, or upload listeners for common cases.
For independent requests, create separate hook instances:
const search = useHttp({
query: '',
})
const upload = useHttp({
file: null,
})
Each instance tracks its own state. A file upload will not block a search request. A failed search will not overwrite an unrelated form error.
This separation is particularly useful in admin panels and multi-panel dashboards. It also fits the wider Laravel ecosystem, where first-party PHP developer tools cover authentication, testing, queues, monitoring, and deployment without forcing every feature into the same request pattern.
422 responses become field errors
Laravel validation errors usually arrive with a 422 Unprocessable Entity response. useHttp parses that response and exposes the messages through errors.
<script setup>
import { useHttp } from '@inertiajs/vue3'
const http = useHttp({
name: '',
email: '',
})
function save() {
http.post('/api/users')
}
</script>
<template>
<form @submit.prevent="save">
<label for="name">Name</label>
<input id="name" v-model="http.name" />
<p v-if="http.errors.name">
{{ http.errors.name }}
</p>
<label for="email">Email</label>
<input id="email" type="email" v-model="http.email" />
<p v-if="http.errors.email">
{{ http.errors.email }}
</p>
<button :disabled="http.processing">
Save
</button>
</form>
</template>
By default, each field receives its first validation message. When an interface needs every message, chain withAllErrors():
const http = useHttp({
name: '',
email: '',
}).withAllErrors()
Now http.errors.name can contain an array of messages. This is useful when a field fails several rules and the interface should show the complete explanation.
You can also respond to different failure types through callbacks:
http.post('/api/users', {
onSuccess: (data, response) => {
console.log(response.status)
},
onError: (errors) => {
console.log(errors)
},
onHttpException: (response) => {
console.log('Server error:', response.status)
},
onNetworkError: (error) => {
console.log('Connection failed:', error.message)
},
onFinish: () => {
console.log('Request finished')
},
})
The distinction matters. A 422 response usually means the user can correct the input. A 500 response needs a different message. A network error may need a retry action.
Precognition brings validation to standalone requests
Laravel Precognition lets you validate a future request before the request creates a side effect. The server runs the relevant validation rules but does not execute the controller action.
Inertia 3.x integrates Precognition directly into useHttp:
const http = useHttp({
name: '',
email: '',
}).withPrecognition('post', '/api/users')
Once enabled, the hook provides methods such as:
validate()touch()touched()valid()invalid()
A Vue form can validate fields as they change:
<script setup>
import { useHttp } from '@inertiajs/vue3'
const http = useHttp({
name: '',
email: '',
}).withPrecognition('post', '/api/users')
</script>
<template>
<input
v-model="http.name"
@change="http.validate('name')"
/>
<p v-if="http.invalid('name')">
{{ http.errors.name }}
</p>
<input
v-model="http.email"
type="email"
@change="http.validate('email')"
/>
<p v-if="http.invalid('email')">
{{ http.errors.email }}
</p>
</template>
On the Laravel side, add HandlePrecognitiveRequests to the route and keep validation rules in a form request:
use App\Http\Requests\StoreUserRequest;
use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;
Route::post('/api/users', function (StoreUserRequest $request) {
return response()->json([
'user' => User::create($request->validated()),
]);
})->middleware(HandlePrecognitiveRequests::class);
Read the full Laravel Precognition documentation for route configuration, custom rules, file handling, and testing.
Precognition keeps validation on the server. Your Vue application does not need to duplicate rules for email formats, unique values, password strength, or nested data.
File uploads include progress tracking
When the request data contains a file, useHttp automatically sends it as multipart/form-data.
The hook exposes upload progress through progress:
<script setup>
import { useHttp } from '@inertiajs/vue3'
const http = useHttp({
file: null,
})
function upload() {
http.post('/api/uploads')
}
</script>
<template>
<input
type="file"
@change="http.file = $event.target.files[0]"
/>
<progress
v-if="http.progress"
:value="http.progress.percentage"
max="100"
/>
<button
:disabled="http.processing"
@click="upload"
>
Upload
</button>
</template>
For large images, documents, and media files, progress is part of the interface rather than an implementation detail. Users can see that the request is active and understand when they can continue.
Precognition does not upload files during validation by default. That prevents the same large file from being sent repeatedly while the user edits other fields. If file validation must run during precognition, configure the request and explicitly enable file validation.

Optimistic updates keep interfaces responsive
Some actions should feel immediate. A like button is a simple example.
With optimistic(), update local data before the server responds:
http
.optimistic((data) => ({
likes: data.likes + 1,
}))
.post('/api/likes')
The update happens synchronously. If the request fails, Inertia rolls the data back.
Optimistic updates work best for small, reversible changes. Use them for reactions, toggles, read states, and lightweight preferences. Avoid them when the server response changes several related records or when failure needs a complex recovery flow.
Cancellation protects fast-changing interfaces
Search inputs and autocomplete controls can create several requests in quick succession. A stale response should not overwrite newer results.
Cancel an in-progress request with:
http.cancel()
You can combine cancellation with a debounce in your component. You can also use separate useHttp instances when several requests should remain independent.
The lifecycle callbacks include onCancel, which lets you restore interface state:
http.get('/api/search', {
onCancel: () => {
console.log('Search cancelled')
},
})
This gives standalone requests the same level of control you expect from other Inertia interactions.
usePoll: Choose the right polling mode
useHttp handles standalone requests. For refreshing props on the current Inertia page, Inertia 3.x provides usePoll.
import { usePoll } from '@inertiajs/vue3'
usePoll(2000, {
only: ['stats'],
})
Polling uses partial reloads. It automatically stops when the component unmounts, and it can be controlled manually:
<script setup>
import { usePoll } from '@inertiajs/vue3'
const { start, stop, polling } = usePoll(
2000,
{
only: ['stats'],
},
{
autoStart: false,
},
)
</script>
<template>
<button v-if="polling" @click="stop">
Pause updates
</button>
<button v-else @click="start">
Resume updates
</button>
</template>
Inertia 3.x also adds polling concurrency modes:
-
overlapstarts a request on every tick, even if the previous one remains active. -
cancelaborts the active request before starting the next one. -
restwaits for the current request to finish, then waits the configured interval.
Use rest when requests must never overlap. Use cancel for rapidly changing data where only the latest result matters. Use overlap when every interval should trigger a request.
Polling is throttled in background tabs by default. Set keepAlive: true when background updates must continue.

Deferred props and rescue slots
Not every prop needs to block the first render.
On the server, wrap expensive work with Inertia::defer():
return Inertia::render('Dashboard', [
'users' => User::all(),
'permissions' => Inertia::defer(
fn () => Permission::all()
),
]);
Inertia resolves the deferred prop in a separate request after the initial page renders. You can group deferred props when several expensive queries should load together.
On the Vue side, use Deferred:
<script setup>
import { Deferred, router } from '@inertiajs/vue3'
</script>
<template>
<Deferred data="permissions">
<template #fallback>
<p>Loading permissions...</p>
</template>
<template #rescue="{ reloading }">
<p>Permissions could not be loaded.</p>
<button
:disabled="reloading"
@click="router.reload({ only: ['permissions'] })"
>
Try again
</button>
</template>
<PermissionsList :items="permissions" />
</Deferred>
</template>
Pass rescue: true to Inertia::defer() when an exception should not fail the entire page. The rescue slot can then provide a retry action for a slow database, unavailable service, or external API timeout.
The reloading value also helps you show a refresh state while keeping existing content visible.
A cleaner boundary for Laravel and Vue
useHttp does not replace router.visit, useForm, or deferred props. It gives each interaction a clearer boundary.
Use page visits when navigation changes the page. Use useForm when a form submission follows Inertia’s page lifecycle. Use useHttp for JSON requests that should stay in place. Use usePoll for recurring partial reloads. Use deferred props when expensive data can wait until after the first render.
That division keeps controllers explicit and frontend state easier to reason about. It also makes Laravel a practical choice when you want to build a REST API with PHP without giving up a polished Vue experience.
Explore the Inertia 3.x HTTP request documentation, review the Laravel documentation, and choose the smallest request tool that matches the interaction. Your SPA will have fewer accidental navigations and more predictable state.