A slow request does not always mean a slow experience.
When a user clicks a favorite button, checks off a todo, or changes a setting, the interface should respond immediately. Waiting for a server round trip before updating the screen makes even a fast application feel heavy.
Inertia 3.x gives Vue developers a focused tool for this pattern: useHttp. It combines standalone JSON requests with reactive state, optimistic updates, automatic rollback, and Laravel validation errors.
This article shows how to use it in a Laravel and Vue application.
Optimistic UI: A faster perceived experience
An optimistic update changes the interface before the server confirms the action.
The browser assumes the request will succeed:
- The user clicks a button.
- The interface updates immediately.
- The request runs in the background.
- The server confirms the change.
- The client keeps the server’s response as the final state.
If the request fails, the interface returns to its previous state.
This approach improves perceived performance because the user sees a response at the moment of interaction. The network request still takes time, but that delay no longer controls the visual feedback.
Optimistic UI works best for small, predictable mutations:
- Toggling a favorite or bookmark
- Marking a todo as complete
- Adding a reaction
- Enabling a preference
- Updating a quantity
- Reordering a lightweight list
It requires one important rule: the client must be willing to be corrected.
The server remains the source of truth.

useHttp in Inertia 3.x: Standalone requests with reactive state
Inertia page visits are useful when a request should update page props or navigate to another page. Not every request needs that lifecycle.
useHttp handles standalone HTTP requests to JSON endpoints. It does not trigger an Inertia navigation or replace the current page. It gives you a form-like API for calls such as:
-
GETrequests for search and autocomplete -
POSTrequests for small mutations -
PUTandPATCHrequests for updates -
DELETErequests for removal actions
The hook also exposes reactive state:
processingerrorshasErrorswasSuccessfulrecentlySuccessfulisDirtyprogress
A Vue component can start with a small local state object:
<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" />
<span v-if="http.processing">
Searching...
</span>
</template>
The endpoint should return JSON, not an Inertia::render() response. This makes useHttp a useful fit when you build a REST API with PHP alongside an Inertia-powered frontend.
You can read the full useHttp documentation and compare it with Inertia’s useForm helper.
Optimistic updates with automatic rollback
The optimistic() method accepts a callback. The callback receives the hook’s current data and returns the fields that should change.
http.optimistic((data) => ({
likes: data.likes + 1,
})).post('/api/likes')
The update happens synchronously. The interface changes before the request completes.
If the server responds successfully, its response becomes the authoritative result. If the request fails, Inertia restores the previous value automatically.
Only return the keys you want to change. Partial updates make the rollback precise and reduce accidental state changes.
The flow looks like this:
Original state
↓
Optimistic state applied immediately
↓
HTTP request sent
↓
Success: keep the server result
Failure: restore the original state
Rollback covers several failure cases:
- Laravel validation errors with a
422response - Authorization or server errors
- Network failures
- Interrupted requests
Inertia also tracks optimistic updates independently. That matters when multiple requests are active at the same time.
Real-world example: Toggle a todo favorite
Assume a todo has a boolean favorite column. The Vue component receives the current todo through page props, but the favorite action itself uses a standalone JSON endpoint.
Laravel endpoint
Define the endpoint in routes/api.php:
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Models\Todo;
Route::middleware('auth:sanctum')->patch('/todos/{todo}/favorite', function (
Request $request,
Todo $todo
) {
abort_unless(
$request->user()->can('update', $todo),
403
);
$validated = $request->validate([
'favorite' => ['required', 'boolean'],
]);
$todo->update([
'favorite' => $validated['favorite'],
]);
return response()->json([
'favorite' => $todo->favorite,
]);
});
Laravel validates the incoming value on the server. It also checks authorization before changing the record.
The endpoint returns the saved value instead of assuming the client’s value was correct. That response matters when business rules, authorization, observers, or database logic modify the result.
Laravel’s validation documentation covers the validation rules used here. For API authentication, see Laravel Sanctum.
Vue component
Create a useHttp instance with the server’s current value:
<script setup>
import { useHttp } from '@inertiajs/vue3'
const props = defineProps({
todo: {
type: Object,
required: true,
},
})
const favorite = useHttp({
favorite: props.todo.favorite,
})
function toggleFavorite() {
favorite
.optimistic((data) => ({
favorite: !data.favorite,
}))
.patch(`/api/todos/${props.todo.id}/favorite`, {
onSuccess: (response) => {
// The server remains the final authority.
favorite.favorite = response.favorite
},
onHttpException: () => {
// Show a toast or inline message.
},
onNetworkError: () => {
// Tell the user that the change could not be saved.
},
})
}
</script>
<template>
<button
type="button"
:aria-pressed="favorite.favorite"
:disabled="favorite.processing"
@click="toggleFavorite"
>
{{ favorite.favorite ? 'Remove favorite' : 'Add favorite' }}
</button>
<p v-if="favorite.errors.favorite">
{{ favorite.errors.favorite }}
</p>
</template>
The button changes as soon as the user clicks it. The processing state can disable repeated clicks while the request is active.
If the server returns a 422 response, Inertia places the validation message in favorite.errors.favorite. The optimistic value rolls back automatically.
That gives the user both kinds of feedback:
- Immediate visual feedback for a successful action
- Clear correction and explanation when the server rejects it

Keep server state as the source of truth
Optimistic UI does not mean trusting the browser.
The client can predict a result. It cannot authorize the action, enforce validation, resolve conflicts, or guarantee that the database accepted the change.
Use these practices to keep the boundary clear:
Return canonical values
Return the saved resource or the authoritative fields from Laravel. Do not assume the submitted value is the final value.
return response()->json([
'todo' => $todo->fresh(),
]);
Validate every mutation
Client-side checks improve interaction, but Laravel validation protects the application. useHttp automatically exposes validation errors returned with status 422.
You can request every message instead of the first message:
const http = useHttp({
title: '',
}).withAllErrors()
Roll back on every failure
Do not hide errors after an optimistic update. The rollback tells the user that the server did not accept the change.
Use onHttpException for responses such as 403 or 500. Use onNetworkError when the request never reaches the server.
Avoid optimistic updates for irreversible actions
Deleting an account, charging a card, or sending a message may need explicit confirmation. Optimistic updates suit reversible, low-risk interactions best.
Deferred props and polling for live dashboards
Optimistic updates handle local interactions. Dashboards often need a second pattern: loading and refreshing server data without blocking the first render.
Deferred props let Laravel postpone expensive data until after the initial page loads:
return inertia('Dashboard', [
'summary' => Inertia::defer(
fn () => DashboardSummary::for($request->user())
),
]);
The critical dashboard shell can render first. The summary arrives in a follow-up request.
In Vue, wrap the deferred prop with the Deferred component:
<Deferred data="summary">
<template #fallback>
<div>Loading dashboard summary...</div>
</template>
<DashboardSummary :summary="summary" />
</Deferred>
For data that changes over time, combine deferred props with usePoll:
<script setup>
import { usePoll } from '@inertiajs/vue3'
usePoll(5000, {
only: ['summary'],
})
</script>
This requests only the changing prop instead of reloading the entire page.
A practical dashboard can use all three tools:
- Deferred props load non-critical metrics after the first render.
-
usePollrefreshes server-owned metrics on a schedule. -
useHttphandles small user actions, such as pinning a widget or changing a filter.

A simple rule for choosing the right Inertia tool
Use an Inertia router visit when the request should update page props or navigate.
Use useForm when you are submitting a form through the Inertia lifecycle.
Use useHttp when you need a standalone JSON request with local reactive state.
Use optimistic updates when the expected result is easy to predict and safe to reverse.
Laravel provides the server-side guarantees. Vue provides the immediate interface. Inertia 3.x connects the two without requiring another client-state library for every small interaction.
For developers evaluating a PHP web framework or assembling a modern set of PHP developer tools, this is the useful distinction: fast interfaces do not require weaker server rules. Let the browser respond quickly, then let Laravel confirm what actually happened.