Laravel Daily's

Optimistic UI in Inertia 3.x: Instant Feedback with useHttp in Laravel + Vue

Bright illustration of Laravel, Vue, Inertia, and PHP powering an optimistic UI update

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:

  1. The user clicks a button.
  2. The interface updates immediately.
  3. The request runs in the background.
  4. The server confirms the change.
  5. 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.

Illustration showing an optimistic UI action updating immediately, with a rollback path on failure

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:

  • GET requests for search and autocomplete
  • POST requests for small mutations
  • PUT and PATCH requests for updates
  • DELETE requests for removal actions

The hook also exposes reactive state:

  • processing
  • errors
  • hasErrors
  • wasSuccessful
  • recentlySuccessful
  • isDirty
  • progress

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 422 response
  • 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

Laravel and Vue todo interface showing a task changing state instantly with validation feedback

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.
  • usePoll refreshes server-owned metrics on a schedule.
  • useHttp handles small user actions, such as pinning a widget or changing a filter.

Bright Laravel and Inertia dashboard illustration with deferred cards and live polling indicators

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.

Previous
Authorization in Inertia 3.x: Policies, Gates, and Permission-Aware Vue Components
Next
Build an AI Content Generation Pipeline in Laravel: Draft, Refine, and Publish with the AI SDK