Los formularios son los los componentes empleados por excelencia para obtener datos por parte del usuario, y son una de las características principales que tenemos en Laravel Inertia, los podemos usar perfectamente manteniendo el esquema clásico de Laravel en el servidor, pero, usando Vue con Inertia desde el cliente.
Trabajar con un formulario en Laravel Inertia es una de las mejores maneras de combinar la solidez del backend de Laravel con la fluidez de Vue en el cliente.
Cuando empecé a trabajar con Inertia, algo que realmente me sorprendió fue lo natural que resulta manejar formularios: se sienten como los formularios tradicionales de Laravel, pero con las ventajas de una SPA, sin tener que construir una API completa.
En esta guía te muestro exactamente cómo implementar un formulario tanto para crear como para editar registros, incluyendo:
- controladores en Laravel
- validación
- props
- manejo de errores
Los formularios son una herramienta fundamental para recolectar información del usuario. Cuando hablamos del proceso de actualización, solemos partir desde un listado en el cual el usuario elige qué registro desea modificar.
Al seleccionar un elemento, la aplicación lo redirige al formulario de edición. Este formulario ya incluye el identificador del registro, que normalmente viaja en la URL y que permite al backend saber qué datos deben cargarse desde la base de datos.
Tal como en cualquier formulario HTML, podemos incluir cualquier tipo de campo: inputs, selects, textareas, uploads, etc.
La diferencia clave con Inertia es que todo fluye como una SPA, sin recargas completas, manteniendo la simplicidad del ciclo de vida típico de Laravel.
Una vez que el usuario ha editado la información del formulario, puede enviar el formulario haciendo clic en un botón "Actualizar" o “Enviar” para que Laravel se encargue de las validaciones y posterior guardado en la base de datos si los datos son correctos o mostrar los errores de validación.
Laravel Inertia no es más que un scaffolding o esqueleto para Laravel, que agrega funcionalidades a un proyecto en Laravel para poder integrar Vue como tecnología del cliente, con Laravel, como tecnología en el servidor.
1. ¿Laravel Inertia y por qué facilita el manejo de formularios?
Inertia es un puente que elimina el típico divorcio entre backend y frontend. Técnicamente no es un framework, sino un scaffolding que permite usar Vue, React o Svelte como vistas, mientras Laravel sigue controlando rutas, controllers y validación.
Cómo se integra Vue y Laravel en un mismo flujo
En mi caso, describo Inertia como “Vue con esteroides”, porque puedo seguir usando mis controladores de Laravel tal cual, pero en vez de devolver una view(), devuelvo un inertia(), que muestra un componente Vue con datos incluidos.
Ventajas reales al crear formularios dinámicos
- Puedes usar v-model como en cualquier app Vue.
- Puedes enviar formularios con router.post/put/delete sin recargar la página.
- Laravel sigue manejando validación con FormRequest.
- El estado del formulario (errores, processing, etc.) lo maneja useForm.
Inertia facilita mucho el proceso de manejo de formularios con Vue, ya que, no tenemos que implementar una Rest Api y mediante el objeto useForm facilita el proceso de validaciones.
2. Preparar el controlador en Laravel para manejar formularios
En Laravel, en un controlador que trabaja con las categorías, vamos a crear los métodos para presentar el componente en Vue, mediante la función de inertia() en vez de la función clásica de view() que permite renderizar un componente en Vue.
Método create() y envío de datos al componente
Y la función de store, para almacenar los datos de la categoría en la base de datos; es decir, crear la categoría:
app/Http/Controllers/Dashboard/CategoryController.php
<?php
namespace App\Http\Controllers\Dashboard;
use App\Http\Controllers\Controller;
use App\Models\Category;
use Illuminate\Http\Request;
class CategoryController extends Controller
{
public function create()
{
return inertia("Dashboard/Category/Create");
}
}Método store() para guardar registros con Inertia
En mis proyectos suelo procesar así:
class CategoryController extends Controller
{
public function store(Request $request)
{
Category::create(
[
'title' => request('title'),
'slug' => request('slug'),
]
);
dd($request->all());
}
}3. Crear un formulario básico en Inertia con Vue
Se crea el formulario con sus v-model respectivos y proceso el submit para enviar el formulario al método anterior:
resources/js/Pages/Dashboard/Category/Create.vue
<template>
<form @submit.prevent="submit">
<label for="">Title</label>
<input type="text" v-model="form.title" />
<label for="">Slug</label>
<input type="text" v-model="form.slug" />
<button type="submit">Send</button>
</form>
</template>Para el apartado de script, usamos la función de useForm() con la cual nos permite manejar de una manera sencilla el formulario, es un helper que, facilita el manejo de errores y estado del formulario en general:
array:10 [
"title" => "Test"
"slug" => "test-slug"
"isDirty" => true
"errors" => []
"hasErrors" => false
"processing" => false
"progress" => null
"wasSuccessful" => false
"recentlySuccessful" => false
"__rememberable" => true
]Enviar datos usando useForm
Finalmente, tenemos el uso de las funciones get, post, put, path o delete, mediante el objeto de Inertia en JavaScript que usamos para enviar el formulario:
<script>
import { router } from '@inertiajs/vue3'
import { useForm } from "@inertiajs/inertia-vue3";
export default {
setup() {
const form = useForm({
title: null,
slug: null,
});
function submit() {
router.post(route("category.store"), form);
}
return { form, submit };
},
};
</script>Manejo de estados: processing, errors, wasSuccessful
Esto lo entrega automáticamente el helper:
form.processing
form.errors
form.wasSuccessfulY se sincroniza sin que tengas que manejar manualmente ningún estado.
Editar registros en base a evento click
Índice de contenido
- 1. ¿Laravel Inertia y por qué facilita el manejo de formularios?
- Cómo se integra Vue y Laravel en un mismo flujo
- Ventajas reales al crear formularios dinámicos
- 2. Preparar el controlador en Laravel para manejar formularios
- Método create() y envío de datos al componente
- Método store() para guardar registros con Inertia
- 3. Crear un formulario básico en Inertia con Vue
- Enviar datos usando useForm
- Manejo de estados: processing, errors, wasSuccessful
- Editar registros en base a evento click
- 1. Modificar el listado para habilitar el modo edición
- 2. Método update en el cliente (Vue + Inertia)
- ¿Qué ocurre aquí?
- 3. Controlador en Laravel
- 4. Definir la ruta de actualización
- Router Methods
- Configuration Options
- Browser History
- Component State
- Preserve Scroll Position
- Custom Headers
- FormData for Files
- The Form Component (Inertia v3)
- Slot Props
- Form Context
- Event Callbacks
- Changes in Inertia v3
- useHttp Hook (Inertia v3)
- Actualizaciones Optimistas en Inertia.js 3
- 1. El Enfoque Tradicional (Pesimista)
- Implementación con router.visit
- Uso en Formularios
Editar un elemento directamente en el listado —sin usar modales ni pantallas adicionales— es una experiencia muy fluida para el usuario y extremadamente sencilla de implementar con Vue e Inertia.
En este caso, la idea es que cuando el usuario haga clic sobre el nombre de un to-do, ese <span> sea reemplazado por un <input> para permitir la edición inline. Al presionar Enter, se envía la actualización al servidor utilizando Inertia.
A continuación te explico paso a paso cómo implementarlo.
1. Modificar el listado para habilitar el modo edición
Archivo: resources/js/Pages/Todo/Index.vue
<li v-for="t in todos" class="border py-3 px-4 mt-2" :key="t">
<span v-show="!t.editMode" @click="t.editMode = true">
{{ t.name }}
</span>
<TextInput
v-show="t.editMode"
v-model="t.name"
@keyup.enter="update(t)"
/>
<button @click="remove" class="float-right">
***
</button>
</li>Explicación
A cada to-do le agregamos una propiedad adicional llamada editMode, la cual no existe en el modelo, sino que se añade dinámicamente del lado del cliente.
- editMode === true → Se muestra el <input> para editar.
- editMode === false → Se muestra el <span> con el nombre del to-do.
Gracias a la reactividad de Vue, cambiar editMode o el name se refleja automáticamente en la interfaz.
Comportamiento
Cuando el usuario hace clic en el <span>, se activa el modo edición.
Cuando presiona Enter dentro del <input>, se ejecuta el método update(t).
2. Método update en el cliente (Vue + Inertia)
update(todo) {
todo.editMode = false;
router.put(route("todo.update", todo.id), {
name: todo.name,
});
},¿Qué ocurre aquí?
- Se desactiva el modo edición (editMode = false).
- Se envía una petición PUT al backend utilizando Inertia.router.put.
- Se envía el nuevo nombre del to-do.
- Esto permite una experiencia fluida y sin recargas completas de página.
3. Controlador en Laravel
Archivo: app/Http/Controllers/TodoController.php
public function update(Todo $todo, Request $request)
{
$request->validate([
'name' => 'required|string|max:255'
]);
Todo::where("id", $todo->id)
->where('user_id', auth()->id())
->update([
'name' => $request->name,
]);
return back();
}Puntos importantes:
- Se valida el campo name.
- Se garantiza que solo el usuario dueño del to-do pueda editarlo (where('user_id', auth()->id())).
- Se actualiza únicamente el nombre.
4. Definir la ruta de actualización
Archivo: routes/web.php
Route::group([
'prefix' => 'todo',
'middleware' => ['auth:sanctum', config('jetstream.auth_session'), 'verified']
], function () {
Route::get('/', [TodoController::class, 'index'])->name('todo.index');
Route::post('store', [TodoController::class, 'store'])->name('todo.store');
Route::put('update/{todo}', [TodoController::class, 'update'])->name('todo.update');
});Router Methods
You can also use the router directly for more controlled requests:
import { router } from '@inertiajs/vue3'
router.get(url, data, options)
router.post(url, data, options)
router.put(url, data, options)
router.patch(url, data, options)
router.delete(url, options)Configuration Options
Browser History
By default, Inertia adds a new entry to the browser history. Use replace to replace the current entry instead of creating a new one:
router.get('/post', data, { replace: true })Component State
By default, visits to the same page force a new page component instance, which clears any local state, such as form inputs, scroll positions, and focus states.
In certain situations, it is necessary to preserve the page component state. For example, when submitting a form, you should preserve the form data in case validation errors reappear.
Visits to the same page create a new component instance, which clears the local state (form inputs, scroll, focus). To preserve the state:
router.get('/post', data, { preserveState: true })Preserve Scroll Position
When navigating between pages, Inertia mimics the browser's default behavior by automatically resetting the document body scroll position (i.e., when sending a request, the scroll resets to the top); to prevent this, the preserveScroll option is used:
Inertia automatically resets the scroll to the top when navigating
router.put('/update', data, { preserveScroll: true })Custom Headers
The headers option allows you to add custom headers; for example:
router.post('/post', data, {
headers: {
'Custom-Header': 'value',
},
})FormData for Files
When sending files, you need to tell Inertia to use FormData:
router.post('/upload', {
image: file,
}, {
forceFormData: true,
})The Form Component (Inertia v3)
Inertia v3 introduces the <Form> component which automatically handles form state, validation errors, and processing states.
<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" />
<div v-if="errors.name">{{ errors.name }}</div>
<button type="submit" :disabled="processing">
{{ processing ? 'Saving...' : 'Save' }}
</button>
<span v-if="recentlySuccessful">Saved successfully!</span>
</Form>
</template>Slot Props
- errors - Object with validation errors (uses dot notation)
- hasErrors - Boolean indicating if there are errors
- processing - Boolean while the request is in progress
- progress - File upload progress
- wasSuccessful - Boolean after a successful response
- recentlySuccessful - Boolean (briefly true, useful for visual feedback)
- isDirty - Boolean indicating unsaved changes
- defaults - Method to update default values
- setError(field, message) - Set error manually
- clearErrors() - Clear all errors
- reset() - Restore to default values
- submit() - Submit form manually
Form Context
To access the form state from child components without passing props:
import { useFormContext } from '@inertiajs/vue3'
const form = useFormContext()
// Available if inside a Form component
<div v-if="form">
<span v-if="form.isDirty">Unsaved changes</span>
<button @click="form.submit()">Send</button>
</div>Event Callbacks
Callbacks are executed at different stages when making the HTTP request:
router.post('/post', data, {
onBefore: (visit) => {},
onStart: (visit) => {},
onProgress: (progress) => {},
onSuccess: (page) => {},
onError: (errors) => {},
onCancel: () => {},
onFinish: (visit) => {},
})- onBefore, when the request starts.
- onProgress, while the request is in progress.
- onSuccess, when a response exists.
- onError, when a problem occurs.
Changes in Inertia v3
In Inertia v3, some events were renamed:
invalid→httpExceptionexception→networkErrorcancel()→cancelAll()
useHttp Hook (Inertia v3)
The useHttp hook provides a modern way to handle HTTP requests independent of navigation:
import { useHttp } from '@inertiajs/vue3'
const http = useHttp({
name: '',
email: '',
})
// Basic requests
http.get('/api/users')
http.post('/api/users', { name: 'John' })
// With options
http.post('/api/users', { name: 'John' }, {
onSuccess: () => console.log('User created'),
})Actualizaciones Optimistas en Inertia.js 3
Las actualizaciones optimistas son una técnica de diseño de interfaces que prioriza la velocidad percibida.
En lugar de esperar a que el servidor confirme que los datos se guardaron, la interfaz se actualiza instantáneamente asumiendo que la operación tendrá éxito.
1. El Enfoque Tradicional (Pesimista)
Normalmente, el flujo es el siguiente: el usuario hace clic, se muestra un indicador de carga, esperamos la respuesta del servidor (latencia, base de datos, envío de correos) y, finalmente, actualizamos la interfaz en el método onSuccess:
resources\js\pages\todo\Index.vue
// *** Actualizaciones normales
const status = (todo: any) => {
// Actualiza ANTES de hacer los cambios en el servidor, pero, si hay problemas en el servidor NO revierte
// todo.status = todo.status == '1' ? '0' : '1';
// Usamos todoStatus(id).url
router.post(todoStatus(todo.id).url, {
status: todo.status == '1' ? '0' : '1',
//status: todo.status,
},
// onFinish se ejecuta siempre, útil para limpiar estados
{
onError: (errors) => {
add('Error de validación o del servidor');
},
onSuccess: () => {
// Revertir loaders si tuvieras
// Actualiza AL APLICAR los cambios en el servidor (Mas lento)
todo.status = todo.status == '1' ? '0' : '1';
}
},
);
};
Si simulamos un retraso con un sleep(2), la experiencia es frustrante: el usuario hace clic y tiene que esperar dos segundos para ver el cambio. Esto no se siente natural:
app\Http\Controllers\TodoController.php
class TodoController extends Controller
{
***
function status(Todo $todo)
{
sleep(2);
$randomError = rand(1, 10) <= 7;
if ($randomError) {
// abort(422, 'Random error occurred - optimistic update will rollback');
// return response()->json([
// 'message' => 'Random error occurred - optimistic update will rollback'
// ], 422);
// // Esto envía el error de vuelta a Inertia correctamente
throw ValidationException::withMessages([
'status' => 'Random error occurred - optimistic update will rollback',
]);
}
Todo::where("id", $todo->id)->where("user_id", auth()->id())->update([
'status' => request('status') == '1'
]);
return redirect(route('todo.index'));
}
} Implementación con router.visit
Inertia.js facilita esto mediante el bloque onOptimistic. Vamos a analizar cómo funciona en un componente de tareas (To-Do) donde queremos cambiar el estado de "completado".
Anteriormente, cambiábamos el estado manualmente, lo que podía dejar la interfaz en un estado inválido si el servidor fallaba:
resources\js\pages\todo\Index.vue
const status = (todo: any) => {
// Actualiza ANTES de hacer los cambios en el servidor, pero, si hay problemas en el servidor NO revierte
todo.status = todo.status == '1' ? '0' : '1';
router.post(todoStatus(todo.id).url, {
status: todo.status,
},Con el nuevo método onOptimistic, Inertia se encarga de la gestión por nosotros:
resources\js\pages\todo\Index.vue
const status = (todo: any) => {
const newStatus = todo.status == '1' ? '0' : '1';
router
.optimistic((props) => ({
todos: props.todos.map((t) =>
t.id === todo.id ? { ...t, status: newStatus } : t
),
}))
*** LO mismo de antes, POST, PUT...
};Es decir:
const status = (todo: any) => {
const newStatus = todo.status == '1' ? '0' : '1';
router
.optimistic((props) => ({
todos: props.todos.map((t) =>
t.id === todo.id ? { ...t, status: newStatus } : t
),
}))
.post(todoStatus(todo.id).url, {
status: newStatus,
}, {
// Usa onError para errores de validación (422)
onError: (errors) => {
add('Error de validación o lógica', 'error');
},
// onFinish se ejecuta siempre, útil para limpiar estados
onFinish: () => {
// Revertir loaders si tuvieras
}
});
};Aquí:
.optimistic((props) => ({
todos: props.todos.map((t) =>
t.id === todo.id ? { ...t, status: newStatus } : t
),
}))Hacemos es la operación optimista, es decir, pensando en que NO va a haber un error en el servidor y por lo tanto aplica el cambio en la UI de manera automática, pero, al igual que sucede con las migraciones en Laravel, ya con esa declaración Inertia sabe que, si suceden errores que es lo que debe de revertir:
return new class extends Migration
{
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
***
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};¿Cómo funciona la "Magia"?
- Clic del usuario: Se ejecuta inmediatamente lo que está dentro de onOptimistic. La interfaz cambia al instante (el icono cambia de estado).
- Petición en segundo plano: El servidor procesa la actualización mientras el usuario ya ve el resultado.
- Manejo de errores (Rollback): Si el servidor devuelve un error (por ejemplo, un error de validación 422 o un error 500), Inertia realiza automáticamente el rollback. No tienes que programar manualmente la lógica para "volver atrás"; el estado vuelve a su valor original basándose en los datos del servidor:
//*** Error servidor abort(422, 'Random error occurred - optimistic update will rollback'); //*** Error servidor con JSON return response()->json([ 'message' => 'Random error occurred - optimistic update will rollback' ], 422); // Esto envía el error de vuelta a Inertia correctamente //*** Error validacion throw ValidationException::withMessages([ 'status' => 'Random error occurred - optimistic update will rollback', ]);
Uso en Formularios
Esta técnica no se limita al router. También puedes emplearla con el hook useForm. Es muy útil, por ejemplo, en un botón de "Like":
- Acción: El usuario da "Like".
- Optimismo: Incrementamos el contador en +1 visualmente.
- Error: Si falla la red, el contador vuelve a su número anterior.
<Form
action="/todos"
method="post"
:optimistic="(props, data) => ({
todos: [...props.todos, { id: Date.now(), name: data.name, done: false }],
})"
>Es muy similar a las migraciones de base de datos: así como tenemos un método up para crear y un down para revertir, la actualización optimista asume el up y ejecuta el down (rollback) automáticamente si algo sale mal.
El uso de onOptimistic nos evita realizar comprobaciones manuales y hace que nuestra aplicación se sienta increíblemente rápida. Recuerda que esta técnica es ideal para operaciones ligeras y frecuentes donde la probabilidad de éxito es alta.
El siguiente paso, es que aprendas como implementar carga de archivos con Drag and Drop en Laravel Inertia.