Índice de contenido
- 1. ¿Qué es 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 y 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
- Opciones de Configuración del Router
- Browser History
- Component State
- Preserve Scroll Position
- Custom Headers
- FormData para Archivos
- El Componente
- Slot Props
- Form Context
- Event Callbacks
- Cambios en Inertia v3
- useHttp Hook (Inertia v3)
- Actualizaciones Optimistas en Inertia.js 3
- 1. El Enfoque Tradicional (Pesimista)
- Implementación con router.optimistic
- Uso en Formularios con useForm
- El Componente de Inertia
- Importación y Estructura
- 2. Uso de Slots y Destructuring
- Ventajas de recentlySuccessful y processing
- Prueba de Funcionamiento
Los formularios son 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: podemos usarlos manteniendo el esquema clásico de Laravel en el servidor mientras aprovechamos Vue con Inertia en 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 usar 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 recargas completas y sin tener que construir una API desde cero.
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 de datos
- props e Inertia
- Manejo de errores de validación
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 que el usuario elige qué registro desea modificar.
Al seleccionar un elemento, la aplicación lo redirige al formulario de edición, el cual ya incluye el identificador del registro. Ese identificador normalmente viaja en la URL y 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: input, select, textarea, inputs de tipo archivo, 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 los datos, puede enviar el formulario haciendo clic en un botón "Actualizar" o "Enviar" para que Laravel se encargue de las validaciones y del posterior guardado en la base de datos, o bien muestre los errores de validación si algo no está correcto.
Laravel Inertia no es más que un scaffolding o esqueleto para Laravel que agrega funcionalidades a un proyecto para poder integrar Vue como tecnología del cliente, manteniendo Laravel como tecnología en el servidor.
1. ¿Qué es 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, controladores 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 un view(), devuelvo un inertia(), que renderiza un componente Vue con los datos ya incluidos.
Ventajas reales al crear formularios dinámicos
- Puedes usar
v-modelcomo en cualquier app Vue. - Puedes enviar formularios con
router.post,router.putorouter.deletesin recargar la página. - Laravel sigue manejando la validación con
FormRequest. - El estado del formulario (errores, processing, etc.) lo gestiona automáticamente el helper
useForm.
Inertia facilita enormemente el proceso de manejo de formularios con Vue, ya que no necesitamos implementar una REST API por separado; el objeto useForm simplifica el proceso de validaciones y el manejo de estado del formulario.
2. Preparar el controlador en Laravel para manejar formularios
En Laravel, dentro de un controlador que trabaja con categorías, creamos los métodos necesarios para presentar el componente en Vue mediante la función inertia(), en vez de la función clásica view(), que renderiza un componente Vue con los datos ya disponibles.
Método create() y envío de datos al componente
El método create() se encarga únicamente de renderizar el componente Vue del formulario de creación. Si necesitas pasar datos al componente —por ejemplo, una lista de categorías padre—, los agregas como segundo argumento del inertia():
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
El método store() recibe los datos enviados desde el formulario Vue. En mis proyectos suelo procesarlo así:
class CategoryController extends Controller
{
public function store(Request $request)
{
Category::create(
[
'title' => request('title'),
'slug' => request('slug'),
]
);
dd($request->all());
}
}Nota: el dd($request->all()) es solo para depuración durante el desarrollo. En producción deberías retornar un redirect() o una respuesta inertia() según corresponda.
3. Crear un formulario básico en Inertia con Vue
Creamos el formulario con sus directivas v-model respectivas y manejamos el evento submit para enviar los datos al método anterior del controlador:
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 useForm(), un helper que facilita el manejo de errores y el estado del formulario en general. Al hacer el dd() en el servidor, verás que junto a los datos del formulario llegan también propiedades internas de Inertia:
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, usamos los métodos get, post, put, patch o delete disponibles en el objeto form de Inertia para enviar el formulario al servidor. También podemos usar el objeto router directamente:
<script>
import { router } from '@inertiajs/vue3'
import { useForm } from "@inertiajs/vue3";
export default {
setup() {
const form = useForm({
title: null,
slug: null,
});
function submit() {
router.post(route("category.store"), form);
}
return { form, submit };
},
};
</script>Nota: en versiones recientes de Inertia v2 y v3, el import de useForm se hace directamente desde '@inertiajs/vue3', no desde '@inertiajs/inertia-vue3' (paquete legacy).
Manejo de estados: processing, errors y wasSuccessful
El helper useForm expone automáticamente las siguientes propiedades reactivas:
form.processing
form.errors
form.wasSuccessfulEstas propiedades se sincronizan sin que tengas que gestionar ningún estado manualmente. Por ejemplo, puedes usar form.processing para deshabilitar el botón de envío mientras la petición está en curso, y form.errors para mostrar los mensajes de validación de Laravel directamente en el template.
Editar registros en base a evento click
Editar un elemento directamente en el listado —sin usar modales ni pantallas adicionales— ofrece una experiencia muy fluida para el usuario y es extremadamente sencillo de implementar con Vue e Inertia.
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 de la base de datos, 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 sin necesidad de código adicional.
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
PUTal backend utilizandorouter.putde Inertia. - Se envía únicamente 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
namecon las reglas de Laravel. - Se garantiza que solo el usuario dueño del to-do pueda editarlo mediante
where('user_id', auth()->id()). - Se actualiza únicamente el campo
name.
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
También puedes usar el router directamente para tener un control más preciso sobre las peticiones:
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)Opciones de Configuración del Router
Browser History
Por defecto, Inertia agrega una nueva entrada al historial del navegador. Puedes usar la opción replace para reemplazar la entrada actual en lugar de crear una nueva:
router.get('/post', data, { replace: true })Component State
Por defecto, las visitas a la misma página fuerzan una nueva instancia del componente, lo que borra cualquier estado local: datos del formulario, posición de scroll y estados de foco.
En ciertas situaciones, es necesario preservar el estado del componente. Por ejemplo, al enviar un formulario con errores de validación, necesitamos que los datos ingresados por el usuario no desaparezcan.
Para preservar el estado del componente entre visitas a la misma página, usa la opción preserveState:
router.get('/post', data, { preserveState: true })Preserve Scroll Position
Al navegar entre páginas, Inertia imita el comportamiento predeterminado del navegador y resetea automáticamente la posición de scroll al inicio del documento. Para evitar esto, usa la opción preserveScroll:
Inertia resetea el scroll automáticamente al navegar; usa
preserveScroll: truepara evitarlo.
router.put('/update', data, { preserveScroll: true })Custom Headers
La opción headers te permite añadir cabeceras HTTP personalizadas a cualquier petición de Inertia:
router.post('/post', data, {
headers: {
'Custom-Header': 'value',
},
})FormData para Archivos
Cuando necesitas enviar archivos, debes indicarle a Inertia que use FormData en lugar del formato JSON predeterminado. Para ello, usa la opción forceFormData:
router.post('/upload', {
image: file,
}, {
forceFormData: true,
})El Componente <Form> de Inertia v3
Inertia v3 introduce el componente <Form>, que maneja automáticamente el estado del formulario, los errores de validación y los estados de procesamiento, eliminando gran parte del boilerplate manual.
<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- Objeto con los errores de validación (usa notación de punto para campos anidados)hasErrors- Booleano que indica si existen erroresprocessing- Booleano activo mientras la petición está en cursoprogress- Progreso de la subida de archivoswasSuccessful- Booleano que se activa tras una respuesta exitosarecentlySuccessful- Booleano que se activa brevemente tras éxito (ideal para feedback visual)isDirty- Booleano que indica si hay cambios sin guardardefaults- Método para actualizar los valores por defecto del formulariosetError(field, message)- Establece un error manualmente en un campoclearErrors()- Limpia todos los errores del formularioreset()- Restaura los valores al estado por defectosubmit()- Envía el formulario de forma programática
Form Context
Si necesitas acceder al estado del formulario desde componentes hijos sin pasar props, puedes usar el composable useFormContext:
import { useFormContext } from '@inertiajs/vue3'
const form = useFormContext()
// Disponible solo dentro de un componente <Form>
<div v-if="form">
<span v-if="form.isDirty">Unsaved changes</span>
<button @click="form.submit()">Send</button>
</div>Event Callbacks
Los callbacks se ejecutan en diferentes etapas del ciclo de vida de la petición HTTP. Son muy útiles para mostrar loaders, notificaciones o para limpiar estado:
router.post('/post', data, {
onBefore: (visit) => {},
onStart: (visit) => {},
onProgress: (progress) => {},
onSuccess: (page) => {},
onError: (errors) => {},
onCancel: () => {},
onFinish: (visit) => {},
})onBefore: se ejecuta antes de que la petición inicie.onProgress: se ejecuta mientras la petición está en curso (útil para barras de progreso).onSuccess: se ejecuta cuando el servidor devuelve una respuesta exitosa.onError: se ejecuta cuando ocurre un problema (errores de validación o de servidor).
Cambios en Inertia v3
En Inertia v3, algunos eventos globales fueron renombrados para mayor claridad:
invalid→httpExceptionexception→networkErrorcancel()→cancelAll()
useHttp Hook (Inertia v3)
El hook useHttp es una novedad de Inertia v3 que ofrece una forma moderna de realizar peticiones HTTP independientes de la navegación —ideal para llamadas a la API sin que Inertia gestione la respuesta como una visita de página:
import { useHttp } from '@inertiajs/vue3'
const http = useHttp({
name: '',
email: '',
})
// Peticiones básicas
http.get('/api/users')
http.post('/api/users', { name: 'John' })
// Con opciones y callbacks
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 callback onSuccess:
resources/js/pages/todo/Index.vue
// *** Actualizaciones normales (enfoque pesimista)
const status = (todo: any) => {
// Actualiza DESPUÉS de recibir la respuesta del servidor (más lento)
router.post(todoStatus(todo.id).url, {
status: todo.status == '1' ? '0' : '1',
},
{
onError: (errors) => {
add('Error de validación o del servidor');
},
onSuccess: () => {
// Actualiza AL CONFIRMAR los cambios en el servidor
todo.status = todo.status == '1' ? '0' : '1';
}
},
);
};
Si simulamos un retraso con un sleep(2) en el servidor, la experiencia resulta frustrante: el usuario hace clic y debe 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) {
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.optimistic
Inertia.js facilita las actualizaciones optimistas mediante el método encadenado optimistic(). 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 antes de la respuesta del servidor, 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 recibir la respuesta, pero NO revierte si hay error
todo.status = todo.status == '1' ? '0' : '1';
router.post(todoStatus(todo.id).url, {
status: todo.status,
},Con el nuevo método optimistic(), Inertia gestiona el rollback automático 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
),
}))
.post(todoStatus(todo.id).url, {
status: newStatus,
}, {
onError: (errors) => {
add('Error de validación o lógica', 'error');
},
onFinish: () => {
// Revertir loaders si los hubiera
}
});
};Aquí, el bloque:
.optimistic((props) => ({
todos: props.todos.map((t) =>
t.id === todo.id ? { ...t, status: newStatus } : t
),
}))Aplica el cambio en la UI de manera inmediata, asumiendo que no habrá errores en el servidor. Pero, al igual que sucede con las migraciones en Laravel, Inertia sabe exactamente qué debe revertir si ocurre un error. La analogía es perfecta: el método optimistic() define el up() del cambio, y si falla, Inertia ejecuta el down() automáticamente:
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
optimistic(). 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 esperado.
- Manejo de errores (Rollback): Si el servidor devuelve un error (por ejemplo, un
422o un500), Inertia realiza automáticamente el rollback. No necesitas programar manualmente la lógica de "volver atrás"; el estado regresa a su valor original basándose en los datos del servidor:// Error de servidor directo abort(422, 'Random error occurred - optimistic update will rollback'); // Error de servidor con JSON return response()->json([ 'message' => 'Random error occurred - optimistic update will rollback' ], 422); // Error de validación de Laravel throw ValidationException::withMessages([ 'status' => 'Random error occurred - optimistic update will rollback', ]);
Uso en Formularios con useForm
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 de inmediato.
- Error: Si falla la red o el servidor, el contador vuelve a su número anterior automáticamente.
<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 optimistic() 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 (cambios de estado, likes, toggles).
El Componente <Form> de Inertia
Vamos ahora con el componente de formulario avanzado que aparece en la documentación oficial y en los mismos componentes que trae el proyecto por defecto, como el de Edit.vue.
Lo implementamos para que veas cómo simplifica todo el manejo de estados del formulario.
Importación y Estructura
Lo primero es importar el componente directamente desde el paquete de Inertia. Este componente actúa como un "wrapper" o envoltorio inteligente que gestiona el estado del formulario internamente:
<Form :action="store()" class="space-y-6">
***
</Form>
import { Head, useForm, Form } from '@inertiajs/vue3'2. Uso de Slots y Destructuring
A diferencia del método manual, aquí aprovechamos el slot por defecto para obtener directamente las variables que necesitamos: errors, processing y recentlySuccessful (entre otras). La documentación oficial y los componentes de ejemplo de Inertia son una excelente referencia.
Fíjate que ya no hace falta declarar errors como un Prop, ya que el componente <Form> nos lo entrega automáticamente a través del contexto del formulario. La sintaxis moderna utiliza la almohadilla #default para acceder a estos datos, o también puedes usar la directiva v-slot:
<Form :action="store()" class="space-y-6"
v-slot="{ errors, processing, recentlySuccessful }"
>
***
<div class="flex items-center gap-4">
<Button type="submit" :disabled="processing">
Save
</Button>
<Transition enter-active-class="transition ease-in-out" enter-from-class="opacity-0"
leave-active-class="transition ease-in-out" leave-to-class="opacity-0">
<p v-show="recentlySuccessful" class="text-sm text-neutral-600">
Saved.
</p>
</Transition>
</div>
</Form>Ventajas de recentlySuccessful y processing
Esta es la parte más potente del esquema moderno:
processing: Se vuelvetrueautomáticamente mientras la petición viaja al servidor, lo que nos permite deshabilitar el botón de envío con:disabled="processing"sin lógica extra.recentlySuccessful: Es un booleano que se activa justo después de una respuesta exitosa y se desactiva automáticamente tras unos segundos. Es perfecto para mostrar ese pequeño texto de "Guardado" con una transición suave, sin necesidad de gestionar timers manualmente.
Prueba de Funcionamiento
Si enviamos el formulario con errores, verás que la petición llega correctamente y los mensajes de validación de Laravel aparecen donde deben. Al crear una categoría correctamente (por ejemplo, "Categoría 2"), el mensaje de éxito aparecerá y desaparecerá automáticamente gracias a la animación que puedes personalizar a tu gusto con Transition de Vue.
Hemos pasado de un formulario manual a uno totalmente automatizado. Ya no declaramos props de errores ni gestionamos timers para los mensajes de éxito; el componente <Form> de Inertia y sus helpers se encargan de todo el trabajo pesado.
El siguiente paso es que aprendas cómo implementar carga de archivos con Drag and Drop en Laravel Inertia.