En este análisis vamos a comparar dos herramientas de scaffolding fundamentales para el ecosistema Laravel: Livewire e Inertia.js. Ambas se instalan sobre Laravel para potenciar nuestras capacidades de desarrollo, pero bajo filosofías distintas.
En el caso de Inertia, utilizaremos Vue.js (aunque tiene equivalentes en React o Svelte), mientras que Livewire es mi opción favorita para entornos administrativos por su estrecha integración con el núcleo de Laravel.
Daremos una comparación entre varias implementaciones como Datatable, carrito de compras, blog, formulario paso por paso para conocer cual tecnología nos viene mejor, si livewire o Inertia.
Datatable
Índice de contenido
- Datatable
- Filosofía de uso: ¿Cuál elegir?
- Implementación de DataTables en Livewire
- Implementación en Inertia.js (Vue 3)
- ⚔️ Conclusiones para el Datatable
- Eventos, Anidamiento y Comunicación entre componentes
- El Concepto de Componente
- Implementación en el Backend (Laravel)
- Gestión del Frontend y Reutilización
- El Acierto del Layout Automático
- Reactividad y Comunicación: El punto crítico
- La complejidad en Livewire
- La claridad en Inertia (Vue 3)
- Veredicto: ¿Cuál es mejor para un "Paso a Paso"?
- To Do List
- Funcionalidades y Experiencia de Usuario
- Tecnologías y Dependencias
- Análisis del Servidor (Lógica Backend)
- Implementación con Inertia
- Implementación con Livewire
- El Reto del Cliente: JS vs. Livewire + Alpine
- Conclusión: ¿Cuál elegir?
- Blog
- El reto del SEO en Aplicaciones SPA
- Rendimiento y Carga de Recursos
- Desarrollo y Complejidad del Código
- Conclusión: ¿Quién se lleva el punto?
- Resultado: Empate.
- Carrito de Compras
- Estructura y Funcionamiento del CRUD
- Lógica del ítem:
- Reactividad: Livewire vs. Inertia
- El problema de Livewire:
- La ventaja de Inertia (Vue/React):
- Los Eventos en Livewire
- Esto NO Es un Componente, ES una Vista, NO Es Modular: Laravel Inertia vs Livewire
- ¿Pero… cómo que no es un componente?
- Desde las bases: componentes en Laravel
- Comparación con Livewire
- Conclusión: ¿Quién gana?
Filosofía de uso: ¿Cuál elegir?
No existe una herramienta mágica; todo depende de las necesidades de tu proyecto:
- Inertia.js (Vue/React/Svelte): Es ideal cuando buscas una interactividad del lado del cliente extremadamente rica. Para mi propia academia, elegí Vue porque el ecosistema de Node.js y sus extensiones para el navegador no tienen rival.
- Livewire: Es imbatible en velocidad de desarrollo para paneles de administración (dashboards) y formularios complejos, ya que te permite programar casi todo en PHP.
Implementación de DataTables en Livewire
Livewire destaca por ser conciso. Al estar ligado directamente al servidor, la comunicación es transparente. En mi implementación, utilizo un sistema de componentes organizado para mantener el código limpio.
Para que el DataTable sea reutilizable, extendemos de una clase personalizada llamada DataTableComponent:
abstract class DataTableComponent extends Component
{
use WithPagination;
public string $sortColumn = 'id';
public string $sortDirection = 'desc';
public function sort(string $column): void
{
$this->sortColumn = $column;
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
}- Clases Abstractas: Utilizamos herencia para definir comportamientos comunes (como la ordenación) sin repetir código en cada listado.
- Query Scopes: En Laravel, implementamos "scopes" en los modelos para centralizar los filtros. Esto nos permite aprovechar la potencia de Eloquent de forma implícita.
- Filtros Elegantes: En lugar de usar múltiples condicionales if, empleamos el método when(). Esto hace que el código sea más legible y eficiente, activando filtros solo cuando el usuario los solicita.
ponent
{
#[URL]
public ?string $search = null;
public array $columns = [
'id' => 'Id',
'title' => 'Title'
];
protected function getAllFilters(): array
{
return [
'search' => $this->search
];
}
protected function getModelClass(): string
{
return Category::class;
}
public $categoryToDelete;
function with(): array{
$categories = Category::
filterDataTable($this->getAllFilters())
->paginate(10);
return [
'categories' => $categories
];
}En Vue, simplemente es un wire:click al servidor:
<button wire:click="sort('{{ $key }}')" class="flex items-center gap-1">Código fuente en:
https://github.com/libredesarrollo/curso-libro-livewire-4
Implementación en Inertia.js (Vue 3)
Con Inertia, la lógica del servidor es similar, pero el lado del cliente se vuelve más "tortuoso". Al no haber una unión tan fuerte como en Livewire, debemos gestionar la comunicación entre componentes de Vue de forma manual.
Para hacer que la ordenación sea reutilizable en Vue, utilizamos Composables. Estos nos permiten compartir lógica reactiva, pero el flujo de eventos es más complejo:
- Un componente hijo (la cabecera de la tabla) emite un evento al hacer clic.
- El padre recibe el evento y actualiza los parámetros.
- Inertia realiza una petición GET al servidor para refrescar los datos sin perder el estado de la página.
Es decir, del:
resources\js\components\shared\DataTable\DataTableHeader.vue
@click="handleSort(key)"Emitimos al padre
resources\js\components\shared\DataTable\DataTable.vue
<DataTableHeader
***
@sort="handleSort"Y al otro padre:
resources\js\pages\dashboard\post\Index.vue
<DataTable
***
@sort="applyFilters">Donde en Livewire, al ser componentes de Livewire, la comunicación es directa, además de que la implementación es Vue es más compleja por los composable y el resto de la misma.
Código fuente en:
https://github.com/libredesarrollo/curso-libro-laravel-inertia-3/
⚔️ Conclusiones para el Datatable
Aquí es donde Livewire (con Flux) gana por goleada frente a la configuración estándar de Inertia:
- Livewire + Flux: Flux es una unión perfecta entre Blade, Tailwind y Laravel. Nos da componentes listos para usar (tablas, botones, calendarios, migas de pan) con una sintaxis limpia. No tenemos que preocuparnos por pasar props complejas; simplemente definimos arrays y todo funciona.
- Inertia: Por defecto, los componentes que vienen en los starters de Inertia suelen ser demasiado atómicos o insuficientes. A menudo terminas implementando DataTables de forma manual, lo cual agrega carga de mantenimiento y lógica innecesaria a tu solución.
Estéticamente, puedes lograr el mismo resultado con ambas tecnologías, pero Livewire es mucho más directo. Como dice el dicho: "el mejor programador no es el que resuelve más problemas, sino el que los evita". Toda la lógica de eventos y comunicación que requiere Vue/Inertia se siente como un "ruido" innecesario cuando lo que buscas es funcionalidad pura ligada al servidor.
Nota del autor: Si quieres profundizar en estas implementaciones, recuerda que este contenido forma parte de mis cursos y libros completos de Laravel. Puedes encontrar todo el material, el código fuente y las guías detalladas en mi Academia.
En definitiva, para este apartado UN ENORME PUNTO PARA LIVEWIRE:
- Livewire: 1
- Inertia: 0
Eventos, Anidamiento y Comunicación entre componentes
Tras comparar el funcionamiento de los DataTables, donde Livewire resultó ganador por su sencillez y menor cantidad de lógica, en este apartado, analizaremos la creación de formularios paso a paso.
El Concepto de Componente
Un componente es una unidad de trabajo modular y reutilizable. En este ejercicio, buscamos modularidad total. Por ejemplo, el formulario de creación de contactos es un componente independiente que puede consumirse:
- Desde una página principal de "paso a paso".
- Dentro de una publicación de blog.
- Como un modal en otra sección.
Tanto Livewire (con sus "componentes vitaminizados") como Inertia (basado en Vue 3, React o Svelte) utilizan este concepto como eje central.
Implementación en el Backend (Laravel)
En la parte de Laravel no hay grandes diferencias, ya que ambas tecnologías se apoyan en el "Laravel base":
- Livewire: Gestiona las validaciones directamente en la clase del componente o mediante objetos de formulario. Al hacer el submit, se aplican las reglas y se ejecuta el CRUD típico.
- Inertia: Aunque el controlador es prácticamente idéntico, solemos delegar las validaciones a un Form Request o clase aparte.
new #[Layout('layouts.contact')] class extends Component {
use WithFileUploads;
public $step = 1;
#[Validate('required|min:2|max:255')]
public $subject;
#[Validate('required|min:2|max:255')]
public $message;
#[Validate('required')]
public $type = 'person';
public $contactGeneral;Gestión del Frontend y Reutilización
Aquí es donde realmente medimos la facilidad para adaptar y reutilizar componentes.
El Acierto del Layout Automático
Ambas tecnologías logran algo muy inteligente: la detección del Layout.
- Inertia: Si un componente se consume mediante una ruta, carga el layout definido; si se consume como un componente hijo dentro de otra página de Vue, el layout no se carga, evitando duplicidad.
- Livewire: Sucede lo mismo. Si se invoca como un componente dentro de otra vista Blade (por ejemplo, en un @livewire('show-contact')), el layout principal no se renderiza.
Esto es una ventaja enorme frente al Laravel base, donde un @extends en Blade cargaría la plantilla maestra sin importar cómo se consuma la vista.
Reactividad y Comunicación: El punto crítico
La complejidad en Livewire
En Livewire, la reactividad a veces puede ser confusa debido al acoplamiento entre PHP y JavaScript.
- Eventos: Para comunicar un hijo con el padre, usamos eventos. Sin embargo, al leer el código, a veces no queda claro quién está escuchando ese evento si no tienes el árbol de componentes en mente.
#[On('stepEvent')] public function stepEvent($step) { $this->step = $step; } *** <flux:button wire:click="$dispatch('stepEvent',[1])">Back</flux:button>
- Alpine.js e Interacción: Livewire utiliza Alpine.js para la reactividad en el cliente. Aunque es potente, mezclar wire:model, wire:get y objetos de Alpine puede generar "ruido" visual y mental para quien empieza.
<div x-data="{ active:$wire.entangle('step') }" class="flex mx-auto flex-col sm:flex-row">
- Limitación: Un punto negativo es que, si un componente hijo no ha sido renderizado (por un condicional), no puede recibir eventos del padre fácilmente, lo que limita la fluidez.
La claridad en Inertia (Vue 3)
Inertia ofrece una experiencia más modular y predecible:
- Separación de conceptos: El controlador devuelve datos y Vue se encarga del 100% de la lógica del cliente.
- Props y Eventos: Al usar Vue, la comunicación mediante props y emits es estándar. Si ves un evento, sabes exactamente dónde está definido en el código JavaScript.
<ContactCompany :contactGeneralId="contactGeneral.id" @back-step-event="backStep" v-if="$page.props.step == 2" :contactCompany="contactGeneral.company" />
Reactividad Real: Vue es extremadamente inteligente. Incluso si un componente está bajo un v-if, en el momento de renderizarse, la comunicación y el estado se sincronizan de forma natural, es decir, podemos comunicar el componente hijo al padre AUNQUE AL MOMENTO DE CARGAR LA página el componente hijo NO se cargue como en el bloque de código anterior.
- El Livewire, NO es posible, si un componente hijo NO se carga al cargar la página, entonces NO puedes enviar mensajes del hijo al padre:
<livewire:contact.company :parent-id="$contactGeneral->id" /> *** company.blade.php // No funciona porque sus hijos no estan renderizados por el @if/blade #[On('parentId')] public function setParentId(int $parentId): void { $this->parentId = $parentId; if ($this->parentId) { $modelClass = $this->getModelClass(); $this->model = $modelClass::where('contact_general_id', $this->parentId)->first(); $this->setModelData($this->model); } }
Veredicto: ¿Cuál es mejor para un "Paso a Paso"?
En este ejercicio específico, Inertia (Vue) se lleva el punto por las siguientes razones:
- Depuración más sencilla: Al no estar tan fuertemente acoplado el servidor con el cliente en cada pequeña interacción, es más fácil analizar dónde falla algo.
- Manejo de Errores: En Inertia, el objeto form de Vue maneja los errores de forma local y modular. En Livewire, si no se gestiona bien, el objeto de errores global puede causar conflictos al anidar múltiples formularios.
- Expresividad: La sintaxis de Vue para manejar estados complejos (como el paso actual de un formulario) resulta más natural y menos propensa a errores de "ida y vuelta" (roundtrip) innecesarios al servidor.
- Resumen: Mientras que Livewire ganó en el DataTable por su velocidad de implementación, Inertia gana en componentes complejos y anidados por su robustez y reactividad real.
En definitiva, aunque yo soy TEAM Livewire, Inertia se merece su puntazo en este módulo:
- Livewire: 1
- Inertia: 1
To Do List
Vamos a comparar dos implementaciones de una aplicación de Todo List: una desarrollada con Inertia.js (utilizando Vue) y otra con Livewire (apoyado en Alpine.js). La aplicación es sencilla pero funcional: cuenta con los típicos CRUD (Crear, Leer, Actualizar, Borrar) centralizados en una sola ventana para evaluar qué flujo de desarrollo resulta más eficiente.
Funcionalidades y Experiencia de Usuario
En ambas versiones tenemos edición en línea. Por ejemplo, al seleccionar una tarea, podemos editarla y los cambios persisten directamente en la base de datos.
- Gestión de Tareas: Marcado y desenmarcado de ítems, creación de nuevas tareas y eliminación.
- Diseño y Estilos: El diseño varía ligeramente. En una utilicé componentes predefinidos, mientras que en la otra hice un estilo con Tailwind CSS desde cero.
- Interactividad: Ambas cuentan con sistema de Drag & Drop para el reordenado de tareas y búsqueda en tiempo real (en línea).
Tecnologías y Dependencias
El enfoque principal de esta comparativa es evaluar el desarrollo en el cliente: Vue.js frente a Livewire + Alpine.js.
- Backend: En ambos casos es muy similar, basándose en controladores clásicos de Laravel.
- Frontend: Utilizamos Sortable.js como dependencia externa para el reordenamiento.
- Origen del proyecto: La versión de Livewire nació de una aplicación que primero hicimos 100% en Alpine.js (disponible en mis libros y cursos), para luego demostrar cómo adaptarla e integrarla con el servidor de forma transparente.
Análisis del Servidor (Lógica Backend)
Implementación con Inertia
En Inertia trabajamos con controladores tradicionales. Tenemos métodos para obtener el listado, crear y actualizar. Incluimos lógica de ordenación mediante un foreach que actualiza la posición de los IDs recibidos y siempre filtramos por el usuario autenticado para garantizar la seguridad.
Implementación con Livewire
Aquí utilizamos un componente "todo-en-uno". Definimos las reglas de validación, el método mount para cargar los datos y las funciones de save, delete y setPositions. El modelo no tiene mayor misterio; es una estructura sencilla que funciona igual para ambas tecnologías.
El Reto del Cliente: JS vs. Livewire + Alpine
El verdadero reto está en el lado del cliente.
- Alpine.js: Definimos un bloque x-data. En este proyecto, al ser una lógica compleja, la extraje a una función aparte. Utilizamos la directiva @script para cargar el JavaScript de Livewire correctamente y evitar errores de renderizado. Lo mejor es la comunicación directa: usamos eventos de Livewire para despachar acciones al servidor de forma transparente, sin necesidad de configurar rutas manuales o usar Axios/Fetch.
<div x-data="data()" x-init="order()" class="max-w-xl mx-auto py-8"> <flux:card> *** <div class="mt-6"> <ul x-ref="items" class="space-y-2" wire:ignore> <template x-for="t in filterTodo()" :key="t.id"> <li :id="t.id" class="flex items-center gap-3 p-3 bg-zinc-50 dark:bg-zinc-800 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors"> <input type="checkbox" x-model="t.status" @change="$wire.dispatch('update', { todo: t })" class="w-5 h-5 rounded border-zinc-300 text-purple-600 focus:ring-purple-500" > <div class="flex-1"> <template x-if="completed(t)"> <span class="text-green-600 text-sm font-medium">Completado</span> </template> <template x-if="!completed(t)"> <span class="text-orange-600 text-sm font-medium">Pendiente</span> </template> <span x-text="t.name" @click="t.editMode=true" x-show="!t.editMode" class="block mt-1"></span> <flux:input type="text" @keyup.enter="t.editMode=false; $wire.dispatch('update', { todo: t })" x-model="t.name" x-show="t.editMode" class="mt-1" /> </div> <flux:button variant="danger" size="sm" @click="remove(t)" icon="trash"> </flux:button> </li> </template> </ul>
- Vue.js: Para muchos es más elegante por ser un framework completo. Importamos componentes, definimos props para el listado de tareas y manejamos el formulario con el helper form.post. Aunque es más estructurado y mantiene una separación clara entre cliente y servidor, requiere definir rutas en web.php y gestionar un archivo de rutas JS generado automáticamente.
<template> <WebLayout> <o-modal v-model:active="confirmDeleteActive"> <p class="p-4 text-black"> Are you sure you want to delete the record? </p> <div class="flex flex-row-reverse gap-2 bg-gray-100 p-3"> <o-button variant="danger" @click="remove">Delete</o-button> <o-button @click="confirmDeleteActive = false">Cancel</o-button> </div> </o-modal> <div class="mycard mx-auto mt-10 max-w-2xl"> <div class="mycard-body"> <div class="mb-4 flex items-center justify-between"> <h3 class="text-2xl font-bold">App To Do</h3> <Button variant="destructive" size="sm" @click="removeAll" class="gap-2"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4"> <path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" /> </svg> Delete All </Button> </div> <form @submit.prevent="create" class="mb-2 flex gap-2"> <div class="flex-1"> <Input :class="{ 'border-red-500 bg-red-50': errors.name && todoSelected == 0, }" v-model="form.name" placeholder="What needs to be done?" /> </div> <Button :disabled="form.processing">Send</Button> </form> <InputError v-if="todoSelected == 0" :message="errors.name" /> <ul ref="todoListRef" class="mt-6"> <li v-for="element in dtodos" :key="element.id" class="group mt-2 flex items-center rounded-lg border bg-white px-4 py-3 shadow-sm dark:bg-gray-800"> <span class="drag-handle mr-2 cursor-grab text-gray-400">::</span> <button @click="status(element)" class="focus:outline-none"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" :stroke="element.status == '1' ? '#10b981' : 'currentColor' " viewBox="0 0 24 24" stroke-width="2" class="size-6 transition-colors"> <path v-if="element.status == '1'" stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /> <path v-else stroke-linecap="round" stroke-linejoin="round" d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /> </svg> </button> <div class="ml-3 flex-1"> <span v-if="!element.editMode" @click="element.editMode = true" class="block w-full cursor-pointer" :class="{ 'text-gray-400 line-through': element.status == '1', }"> {{ element.name }} </span> <Input v-else v-model="element.name" @keyup.enter="update(element)" @blur="element.editMode = false" auto-focus /> <InputError v-if="todoSelected == element.id" :message="errors.name" /> </div> <button class="ml-2 opacity-0 transition-opacity group-hover:opacity-100" @click=" confirmDeleteActive = true; deleteTodoRow = element.id; "> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ef4444" class="size-5"> <path fill-rule="evenodd" d="M16.5 4.478v.227a48.816 48.816 0 0 1 3.878.512.75.75 0 1 1-.256 1.478l-.209-.035-1.005 13.07a3 3 0 0 1-2.991 2.77H8.084a3 3 0 0 1-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 0 1-.256-1.478A48.567 48.567 0 0 1 7.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 0 1 3.369 0c1.603.051 2.815 1.387 2.815 2.951Zm-6.136-1.452a51.196 51.196 0 0 1 3.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 0 0-6 0v-.113c0-.794.609-1.428 1.364-1.452Zm-.355 5.945a.75.75 0 1 0-1.5 0l.5 8.5a.75.75 0 1 0 1.5 0l-.5-8.5Zm4.785 0a.75.75 0 1 0-1.5 0l-.5 8.5a.75.75 0 1 0 1.5 0l.5-8.5Z" clip-rule="evenodd" /> </svg> </button> </li> </ul> </div> </div> </WebLayout> </template>
Conclusión: ¿Cuál elegir?
Si comparamos líneas de código, ambas implementaciones están muy igualadas (alrededor de 250 líneas).
- Inertia (Vue): Es más estructurado y robusto, pero el coste es escribir más código y gestionar más archivos.
- Livewire (Alpine): Es más simple y elegante en su integración. Todo se siente más "mezclado" (Livewire, Alpine y plugins de terceros), lo cual es precisamente la magia de este ecosistema: la velocidad de desarrollo.
Para esta oportunidad, doy un empate a ambos:
- Livewire: 2
- Inertia: 2
Blog
En esta nueva comparativa, vamos a analizar el desarrollo de un Blog (listado y detalle con filtros). Recordemos que ya hemos evaluado otros módulos:
- DataTables: Ganó Livewire por ser más reactivo y sencillo de mantener.
- Componentes de Saneamiento (paso a paso): El punto fue para Inertia, ya que al ser componentes de lado del cliente, Vue o React superan a Alpine.js en potencia.
- Todo List: Declaramos un empate técnico.
El reto del SEO en Aplicaciones SPA
Un punto crítico al desarrollar un blog es el SEO. Tradicionalmente, tecnologías de cliente como Vue, React o Svelte tienen problemas porque el contenido se carga de forma asíncrona mediante JavaScript, y Google no siempre indexa correctamente lo que no está en el HTML inicial.
Sin embargo, Inertia.js soluciona esto con SSR (Server Side Rendering). Al usar la función Inertia::render en lugar del view() tradicional de Laravel, el servidor procesa el componente y envía los datos (título, metadatos y contenido) ya renderizados. Esto permite que Google lea el contenido apenas carga la página, sin esperar a que se ejecute el JavaScript del cliente (como el método onMounted).
Por su parte, Livewire no tiene este problema, ya que es una tecnología que nace en el servidor y renderiza HTML directamente desde el inicio.
Rendimiento y Carga de Recursos
Un blog debe ser ligero. En mi experiencia personal (como en mi blog Desarrollo Libre), intento que la página no se bloquee por recursos externos.
- Inertia: Carga el core de Vue e Inertia, lo cual añade algo de peso en JS, aunque es manejable.
- Livewire: Carga su propio script de JavaScript.
En mi caso, incluso usando Livewire, prefiero excluir sus scripts en partes que no requieren reactividad y usar Alpine.js para detalles mínimos (como iluminar un botón). Cargo todo el JS no esencial (publicidad, scripts secundarios) de forma asíncrona para que la experiencia del usuario sea instantánea.
Desarrollo y Complejidad del Código
Al comparar el código de ambos para el módulo de Blog:
- Lógica de Servidor: Es idéntica. Ambos aprovechan la potencia de Laravel (como los Scopes de Eloquent para filtros), por lo que aquí no hay un ganador claro.
- Longitud del Código (Vista/Componente):
- Livewire: Gana en simplicidad. El archivo show.blade.php tiene unas 87 líneas que contienen toda la lógica y el diseño.
- Inertia: El componente de Vue sube a unas 102 líneas, a lo que hay que sumar el código del controlador.
A pesar de que Inertia es un poco más extenso, nos da acceso a todo el ecosistema de plugins de Vue, lo cual es una ventaja implícita.
Conclusión: ¿Quién se lleva el punto?
Aunque un blog es un desarrollo sencillo que no siempre requiere tecnologías tan "pro", es importante saber que tanto Inertia como Livewire son perfectamente capaces de manejarlo.
Inertia ha demostrado que puede competir en el terreno del SEO gracias al SSR, y Livewire sigue siendo el rey de la velocidad de desarrollo en el ecosistema Laravel. Debido a que ambos resuelven el problema de forma excelente con niveles de complejidad similares para este módulo:
Resultado: Empate.
Con esto, el marcador global queda 3 a 3. Ambos frameworks demuestran ser herramientas todoterreno, y la elección dependerá más de tu preferencia personal por el ecosistema (Vue/React vs. Blade/Alpine) que de las limitaciones de la tecnología.
- Livewire: 3
- Inertia: 3
Carrito de Compras
Vamos con otro de los módulos, y creo que será el último que trabajaremos: el del carrito. El funcionamiento se basa en la filosofía de mi blog, donde manejo distintos tipos de publicaciones: posts normales, cursos, libros y anuncios.
Para los ítems de tipo "publicidad" (que en este contexto actúan como productos), empleamos el carrito de compras. Al intentar adquirir uno, aparece un modal que permite añadir el producto. Lo verdaderamente interesante aquí es la comunicación entre componentes, similar a lo que explicamos en el módulo de "paso a paso".
Estructura y Funcionamiento del CRUD
Si analizamos la estructura en la librería, tenemos el componente principal Cart, que se utiliza para la página única (equivalente a lo que manejamos en Inertia). Dentro de este, empleamos otro componente llamado CartItem.
Lógica del ítem:
El CartItem es el que realmente gestiona el CRUD del proyecto. Un ítem puede modificarse para aumentar su cantidad o eliminarse (estableciendo la cantidad en cero).
- Gestión en el Frontend: Utilizamos un array donde el post_id sirve como referencia única. Esto evita tener que iterar todo el carrito cada vez que queremos saber si un producto ya existe; simplemente consultamos por su identificador.
- Gestión en el Backend: En el controlador de Inertia, la lógica se mantiene idéntica. No hay un "ganador" claro en el backend, ya que ambos usan la base de Laravel; solo cambia si defines la lógica en un controlador tradicional o en una clase de componente de Livewire.
<?php
use Livewire\Component;
use Livewire\Attributes\Layout;
use Flux\Flux;
use App\Models\Post;
use App\Models\ShoppingCart;
new #[Layout('layouts.web')] class extends Component
{
protected $listeners = ['itemDelete' => 'getTotal', 'itemAdd' => 'getTotal', 'itemChange' => 'getTotal'];
public $type = 'list';
public $post;
public $cart;
public $total;
function mount(?Post $post, $type = 'list')
{
$this->type = $type;
$this->post = $post;
$this->cart = session('cart', []);
$this->getTotal();
}
function addItem(Post $post)
{
$cart = session('cart', []);
$cart[$post->id] = [$post->id, 'count' => 1];
session(['cart' => $cart]);
$this->dispatch('itemAdd');
}
public function getTotal()
{
if (auth()->check()) {
$this->total = ShoppingCart::where('user_id', auth()->id())->sum('count');
}
}
};
?>
<div class="max-w-2xl mx-auto py-8 space-y-6">
<flux:heading level="1" class="text-center">Carrito de Compras</flux:heading>
@if ($type == 'list')
<flux:card>
@forelse ($cart as $c)
@livewire('shop.cart-item', ['postId' => $c[0]['id']])
@empty
<flux:text class="text-center text-zinc-500 py-8">
Tu carrito está vacío
</flux:text>
@endforelse
</flux:card>
{{ view('pages.shop.partials.shop-cart', ['total' => $total]) }}
@else
<flux:card>
<div class="flex items-center gap-4">
<div class="flex-1">
@livewire('shop.cart-item', ['postId' => $post->id])
</div>
<flux:button
variant="primary"
wire:click="dispatch('addItemToCart', { post: {{ $post->id }} })"
icon="shopping-cart"
>
Comprar
</flux:button>
</div>
</flux:card>
@endif
</div>
Reactividad: Livewire vs. Inertia
Aquí es donde encontramos el "talón de Aquiles" de Livewire.
El problema de Livewire:
En Livewire, la comunicación no es automática. Si modificas un CartItem, debes notificar al componente padre para que recargue el total o cambie la interfaz (por ejemplo, mostrar el panel de compra).
A diferencia de los frameworks de JS, Livewire no tiene una reactividad real, sino una "simulada" mediante peticiones al servidor. Esto hace que la sincronización entre componentes sea más manual y, a veces, propensa a fallos visuales si no se gestionan bien los eventos.
La ventaja de Inertia (Vue/React):
En Inertia, al usar Vue, la reactividad es natural. No necesitas emitir eventos manualmente para actualizar el total; simplemente realizas la operación (un router.post) y, al actualizarse el estado, todos los componentes que dependen de esos datos se refrescan "mágicamente". Es mucho más limpio y eficiente en la parte del cliente.
Los Eventos en Livewire
Lo que menos me gusta de Livewire es su sistema de eventos (dispatch). Aunque es potente, se vuelve extremadamente abstracto.
- Falta de referencia: Puedes emitir un evento, pero no siempre es claro qué componente lo está escuchando. Si retomas un proyecto meses después, pierdes la pista de dónde se definió la función que responde a ese evento.
- Bajo acoplamiento: Es bueno porque permite comunicar cualquier componente, pero es malo porque genera una estructura desorganizada ("una rochela") donde los eventos pueden dispararse desde cualquier parte sin una jerarquía clara.
Comparativa de Código y Organización
A nivel de líneas de código, ambos están muy parejos (alrededor de 160 líneas para los componentes más complejos).
Lo que me gusta de Livewire:
Me parece que está mejor estructurado en cuanto a la organización de archivos. Por ejemplo, las páginas se alojan en su propia carpeta Pages, separadas de los componentes reutilizables. Livewire te guía más hacia una estructura organizada por defecto, similar a cómo Laravel organiza modelos y controladores.
$this->dispatch('itemAdd');Esto NO Es un Componente, ES una Vista, NO Es Modular: Laravel Inertia vs Livewire

Esto que tenemos NO es un componente en Laravel Inertia. Al menos, no es un componente per se en el contexto de nuestra aplicación.
¿A qué demonios me refiero?, te pudieras preguntar.
Te voy a dar aquí un poquito de contexto. Obviamente, esto es —como quien dice— una interpretación propia, una vez que uno ha desarrollado aplicaciones usando Laravel Inertia, que es justamente lo que estamos viendo.
Laravel, entre otras tecnologías, se asocia de cierta forma a ciertos conceptos, y uno de ellos es el de componente. Pero, otra vez, esto es más que todo una interpretación personal. Yo te voy a indicar por qué considero que esto no es un componente, sino simplemente una vista:
<contact-layout>
<general-form :errors="errors" :contactGeneral="contactGeneral"/>
<company-form :contactCompany="contactGeneral.company"/>
</contact-layout>
¿Pero… cómo que no es un componente?
Lo primero que tú pudieras decirme es:
“¡Mira, pero obviamente eso es un componente! ¡Es un bendito componente en Vue! ¿No lo estás viendo, por Dios?”
Y sí… el código anterior es un .vue, tenemos un componente que estamos importando, lo llamamos Contact, obviamente tiene su <template> y su parte de <script>. Por lo tanto, tú puedes decir con toda razón:
“Eso es un componente en Vue.”
Y sí, es correcto.
Pero...
Para que se entienda un poquito mi punto de vista (que como te digo te voy a comparar más adelante), es importante ver cómo se comportan tecnologías hermanas como Livewire e Inertia. Y por más odiosas que sean las comparaciones, hay que hacerlas.
¿A qué me refiero con esto? Que lo que estamos usando en Inertia, es directamente una vista.
Recordemos que una de las características principales que tenemos en Laravel Inertia es el uso de Vue, o mejor dicho, la posibilidad de usar Vue. Porque incluso, todavía podemos devolver una vista Blade en lugar de una vista Vue. Así de simple.
Desde las bases: componentes en Laravel
Para que se entienda aún más claramente mi punto, vamos a ir a las bases. En Laravel, los componentes usualmente se empleaban para definir, por ejemplo, botones. Es decir, elementos simples que no requerían lógica adicional.
Podíamos tener algo como:
- Componentes anónimos, más organizados que una vista suelta.
- Componentes con clase, que sí requerían algo de inicialización lógica.
Entonces, con esos conceptos claros, te pregunto:
¿Qué es lo que tú crees que tenemos en el código anterior?
Porque si nos fijamos bien, en Inertia estamos obligados a pasarle la data manualmente para que el componente funcione. Esto se debe a que, al ser tecnologías independientes, no hay una integración profunda, y probablemente nunca la haya.
Comparación con Livewire
En el ejemplo de Livewire:
@livewire('contact.company', ['parentId' => $pk])Solamente le pasamos un identificador para construir el componente. Con ese ID, internamente se busca la clase relacionada:
function mount($parentId)
{
$this->parentId($parentId);
}
function parentId($parentId)
{
$this->parentId = $parentId;
$c = ContactCompany::where('contact_general_id', $this->parentId)->first();
if ($c != null) {
$this->name = $c->name;
$this->identification = $c->identification;
$this->extra = $c->extra;
$this->choices = $c->choices;
$this->email = $c->email;
}
}
public function render()
{
return view('livewire.contact.company');
}
—que en este ejemplo podría ser ContactCompany—. Automáticamente, Livewire ejecuta el método mount, inicializa el componente, y tú no te preocupas por pasarle manualmente toda la data.
Mientras que en el caso de Inertia, además del ID, tienes que pasarle la instancia de la clase, que normalmente se construye en el método edit.
Entonces, en nuestro componente de Inertia (ese .vue que mostramos antes), estamos simplemente importando una vista y pasándole datos para inicializarla manualmente. No hay una instancia de componente Vue como tal con un ciclo de vida controlado desde el servidor.
Por eso es que no lo considero un componente como tal, sino simplemente una vista renderizada con datos.
Conclusión: ¿Quién gana?
Al inicio de este desarrollo, pensé que Livewire ganaría con claridad, pero tras evaluar la reactividad, el resultado es un empate técnico.
- Inertia (Vue/React): Gana en la parte del cliente. Es imbatible para interfaces complejas con mucha comunicación entre componentes, eventos y reactividad fluida.
- Livewire: Es mi favorito para el 90% de los casos. Es excelente para CRUDs, dashboards y aplicaciones donde no hay un anidamiento de componentes tan profundo. Es la herramienta más "generalista" y productiva para un desarrollador Laravel.
Al final, la puntuación queda 4 a 4. No es que una tecnología sea mejor que la otra, sino que debes elegir la que mejor se adapte a tu proyecto. Para la parte de "presentación" al cliente, prefiero la reactividad de Vue; para la gestión interna y rapidez de desarrollo, me quedo con Livewire.
- Livewire: 4
- Inertia: 4