Inertia.js 2.0 representa una evolución significativa en el desarrollo frontend con Laravel. La librería ha sido completamente reescrita para soportar peticiones asíncronas de forma nativa, desbloqueando funcionalidades que antes requerían implementaciones manuales complejas.
En esta guía exploraremos las tres características más impactantes: Prefetching para mejorar la velocidad percibida, Polling para mantener datos actualizados en tiempo real, e Infinite Scroll para gestionar grandes conjuntos de datos de forma elegante.
Requisitos Previos
Para utilizar estas características necesitas Laravel 10+ con PHP 8.1 y las últimas versiones de los adaptadores de Inertia:
composer require inertiajs/inertia-laravel
npm install @inertiajs/vue3Prefetching: Navegación Instantánea
El prefetching permite cargar datos de páginas en segundo plano antes de que el usuario las visite. Cuando finalmente hace clic, la página se muestra instantáneamente porque los datos ya están en caché.
Prefetching Básico con el Componente Link
La forma más sencilla de implementar prefetching es añadiendo el atributo prefetch al componente Link. Por defecto, Inertia precargará los datos cuando el usuario pase el cursor sobre el enlace durante más de 75ms:
<script setup>
import { Link } from '@inertiajs/vue3'
</script>
<template>
<Link href="/users" prefetch>
Usuarios
</Link>
</template>Estrategias de Prefetching
Puedes personalizar cuándo se activa el prefetching según tus necesidades:
<script setup>
import { Link } from '@inertiajs/vue3'
</script>
<template>
<!-- Prefetch al pasar el cursor (hover) - comportamiento por defecto -->
<Link href="/users" prefetch>Usuarios</Link>
<!-- Prefetch al hacer mousedown (click) -->
<Link href="/users" prefetch="click">Usuarios</Link>
<!-- Prefetch al montar el componente -->
<Link href="/users" prefetch="mount">Usuarios</Link>
<!-- Combinación de estrategias -->
<Link href="/users" :prefetch="['mount', 'hover']">Usuarios</Link>
</template>La combinación de mount y hover es especialmente útil: los datos se precargan al cargar la página, pero si el caché expira antes de que el usuario navegue, se volverán a precargar al pasar el cursor.
Control del Caché
Por defecto, los datos se mantienen en caché durante 30 segundos. Puedes personalizar esta duración:
<script setup>
import { Link } from '@inertiajs/vue3'
</script>
<template>
<!-- Caché por 1 minuto -->
<Link href="/users" prefetch cache-for="1m">Usuarios</Link>
<!-- Caché por 10 segundos -->
<Link href="/users" prefetch cache-for="10s">Usuarios</Link>
<!-- Caché por 5000 milisegundos -->
<Link href="/users" prefetch :cache-for="5000">Usuarios</Link>
</template>Patrón Stale-While-Revalidate
Inertia soporta el patrón stale-while-revalidate, que sirve datos del caché inmediatamente mientras actualiza en segundo plano:
<script setup>
import { Link } from '@inertiajs/vue3'
</script>
<template>
<!-- 30s fresh, 1m stale -->
<Link href="/users" prefetch :cache-for="['30s', '1m']">
Usuarios
</Link>
</template>Durante el período "fresh", el caché se devuelve sin hacer peticiones. Durante el período "stale", se muestra el caché pero se hace una petición en segundo plano para actualizar los datos.
Prefetching Programático
Para casos más avanzados como paginación o botones de "cargar más", puedes usar router.prefetch:
<script setup>
import { router } from '@inertiajs/vue3'
import { onMounted } from 'vue'
// Precargar la siguiente página de usuarios
onMounted(() => {
router.prefetch('/users', {
method: 'get',
data: { page: 2 }
}, {
cacheFor: '1m'
})
})
</script>Hook usePrefetch
El hook usePrefetch proporciona información adicional sobre el estado del prefetching:
<script setup>
import { usePrefetch } from '@inertiajs/vue3'
const { lastUpdatedAt, isPrefetching, isPrefetched, flush } = usePrefetch(
'/users',
{ method: 'get', data: { page: 2 } },
{ cacheFor: '1m' }
)
</script>
<template>
<div v-if="isPrefetching">Precargando...</div>
<div v-else-if="isPrefetched">
Datos listos (actualizado: {{ lastUpdatedAt }})
</div>
<button @click="flush">Limpiar caché</button>
</template>Cache Tags para Invalidación
Los cache tags permiten agrupar datos precargados e invalidarlos cuando ocurren eventos específicos:
<script setup>
import { Link, Form } from '@inertiajs/vue3'
</script>
<template>
<Link href="/users" prefetch cache-tags="users">
Usuarios
</Link>
<Link href="/dashboard" prefetch :cache-tags="['dashboard', 'stats']">
Dashboard
</Link>
<!-- El formulario invalida el caché al enviarse exitosamente -->
<Form
action="/users"
method="post"
:invalidate-cache-tags="['users', 'dashboard']"
>
<input type="text" name="name" />
<button type="submit">Crear Usuario</button>
</Form>
</template>Polling: Datos en Tiempo Real
El polling permite actualizar datos automáticamente consultando el servidor a intervalos regulares. Inertia 2.0 simplifica enormemente su implementación con el helper usePoll.
Implementación Básica
<script setup>
import { usePoll } from '@inertiajs/vue3'
defineProps({
notifications: Array,
unreadCount: Number
})
// Actualizar cada 5 segundos
usePoll(5000)
</script>
<template>
<div>
<span>Notificaciones: {{ unreadCount }}</span>
<ul>
<li v-for="notification in notifications" :key="notification.id">
{{ notification.message }}
</li>
</ul>
</div>
</template>Polling Selectivo con Only
Por defecto, usePoll actualiza todas las props de la página. Para optimizar las peticiones, especifica qué props necesitas:
<script setup>
import { usePoll } from '@inertiajs/vue3'
defineProps({
users: Array,
stats: Object,
notifications: Array
})
// Solo actualizar notificaciones, no users ni stats
usePoll(3000, {
only: ['notifications']
})
</script>Callbacks de Ciclo de Vida
Puedes ejecutar código cuando inicia o finaliza cada petición de polling:
<script setup>
import { usePoll } from '@inertiajs/vue3'
import { ref } from 'vue'
const isUpdating = ref(false)
usePoll(5000, {
only: ['stats'],
onStart() {
isUpdating.value = true
console.log('Actualizando estadísticas...')
},
onFinish() {
isUpdating.value = false
console.log('Estadísticas actualizadas')
}
})
</script>
<template>
<div :class="{ 'opacity-50': isUpdating }">
<!-- Contenido -->
</div>
</template>Control Manual del Polling
Para dar control al usuario sobre cuándo se ejecuta el polling:
<script setup>
import { usePoll } from '@inertiajs/vue3'
import { ref } from 'vue'
defineProps({
visitorCount: Number
})
const isLive = ref(true)
const { start, stop } = usePoll(5000, {
only: ['visitorCount']
}, {
autoStart: false
})
const togglePolling = () => {
if (isLive.value) {
stop()
} else {
start()
}
isLive.value = !isLive.value
}
</script>
<template>
<div>
<p>Visitantes: {{ visitorCount }}</p>
<button @click="togglePolling">
{{ isLive ? '🔴 En vivo' : '⚪ Pausado' }}
</button>
</div>
</template>Throttling en Segundo Plano
Por defecto, Inertia reduce la frecuencia de polling en un 90% cuando la pestaña no está visible. Para mantener la frecuencia constante:
<script setup>
import { usePoll } from '@inertiajs/vue3'
// Mantener polling constante aunque la pestaña esté en segundo plano
usePoll(5000, {
only: ['criticalData']
}, {
keepAlive: true
})
</script>Ejemplo Práctico: Dashboard con Estadísticas en Vivo
Este es el controlador en Laravel:
<?php
namespace App\Http\Controllers;
use App\Models\Order;
use App\Models\User;
use Inertia\Inertia;
class DashboardController extends Controller
{
public function index()
{
return Inertia::render('Dashboard', [
'stats' => fn () => [
'totalUsers' => User::count(),
'totalOrders' => Order::count(),
'revenue' => Order::sum('total'),
'pendingOrders' => Order::where('status', 'pending')->count(),
],
'recentOrders' => fn () => Order::with('user')
->latest()
->take(5)
->get(),
]);
}
}Y el componente Vue:
<script setup>
import { usePoll } from '@inertiajs/vue3'
defineProps({
stats: Object,
recentOrders: Array
})
// Actualizar stats cada 10 segundos, pedidos cada 30 segundos
usePoll(10000, { only: ['stats'] })
usePoll(30000, { only: ['recentOrders'] })
</script>
<template>
<div class="grid grid-cols-4 gap-4">
<div class="stat-card">
<h3>Usuarios</h3>
<p>{{ stats.totalUsers }}</p>
</div>
<div class="stat-card">
<h3>Pedidos</h3>
<p>{{ stats.totalOrders }}</p>
</div>
<div class="stat-card">
<h3>Ingresos</h3>
<p>{{ stats.revenue }}€</p>
</div>
<div class="stat-card">
<h3>Pendientes</h3>
<p>{{ stats.pendingOrders }}</p>
</div>
</div>
<div class="mt-8">
<h2>Pedidos Recientes</h2>
<ul>
<li v-for="order in recentOrders" :key="order.id">
{{ order.user.name }} - {{ order.total }}€
</li>
</ul>
</div>
</template>Infinite Scroll: Carga Infinita de Contenido
El infinite scroll carga automáticamente más contenido a medida que el usuario hace scroll, reemplazando los controles de paginación tradicionales. Es ideal para feeds sociales, catálogos de productos y listas extensas.
Configuración del Backend
Utiliza el método Inertia::scroll() para configurar los datos paginados:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Inertia\Inertia;
class PostController extends Controller
{
public function index()
{
return Inertia::render('Posts/Index', [
'posts' => Inertia::scroll(fn () => Post::with('author')
->latest()
->paginate(15))
]);
}
}El método Inertia::scroll() funciona con paginate(), simplePaginate() y cursorPaginate(), además de API Resources de Eloquent.
Implementación en el Frontend
El componente InfiniteScroll detecta automáticamente cuándo el usuario se acerca al final del contenido:
<script setup>
import { InfiniteScroll } from '@inertiajs/vue3'
defineProps({
posts: Object
})
</script>
<template>
<InfiniteScroll data="posts">
<article v-for="post in posts.data" :key="post.id" class="post-card">
<h2>{{ post.title }}</h2>
<p>{{ post.excerpt }}</p>
<span>Por {{ post.author.name }}</span>
</article>
</InfiniteScroll>
</template>Buffer de Carga Anticipada
Controla cuántos píxeles antes del final debe comenzar la carga:
<template>
<!-- Comenzar a cargar 500px antes del final -->
<InfiniteScroll data="posts" :buffer="500">
<article v-for="post in posts.data" :key="post.id">
{{ post.title }}
</article>
</InfiniteScroll>
</template>Sincronización de URL
Por defecto, la URL se actualiza con el número de página visible (?page=3). Para desactivar este comportamiento:
<template>
<InfiniteScroll data="posts" preserve-url>
<!-- Contenido -->
</InfiniteScroll>
</template>Esto es útil cuando el infinite scroll es para contenido secundario como comentarios en un artículo.
Dirección de Carga
Controla si se carga contenido hacia adelante, hacia atrás o en ambas direcciones:
<template>
<!-- Solo cargar siguiente página -->
<InfiniteScroll data="posts" only-next>
<!-- Feed de posts -->
</InfiniteScroll>
<!-- Solo cargar página anterior (para chats) -->
<InfiniteScroll data="messages" only-previous>
<!-- Mensajes -->
</InfiniteScroll>
<!-- Ambas direcciones (por defecto) -->
<InfiniteScroll data="items">
<!-- Lista -->
</InfiniteScroll>
</template>Modo Reverso para Chats
Para interfaces donde el contenido más reciente está abajo, como aplicaciones de chat:
<script setup>
import { InfiniteScroll } from '@inertiajs/vue3'
defineProps({
messages: Object
})
</script>
<template>
<InfiniteScroll data="messages" reverse>
<div v-for="message in messages.data" :key="message.id">
<strong>{{ message.user.name }}:</strong>
{{ message.content }}
</div>
</InfiniteScroll>
</template>El modo reverso hace scroll automático al final al cargar. Para desactivarlo:
<template>
<InfiniteScroll data="messages" reverse :auto-scroll="false">
<!-- Mensajes -->
</InfiniteScroll>
</template>Modo Manual con Botón "Cargar Más"
Para dar control explícito al usuario:
<template>
<InfiniteScroll data="posts" manual>
<template #previous="{ loading, fetch, hasMore }">
<button
v-if="hasMore"
@click="fetch"
:disabled="loading"
>
{{ loading ? 'Cargando...' : 'Cargar anteriores' }}
</button>
</template>
<article v-for="post in posts.data" :key="post.id">
{{ post.title }}
</article>
<template #next="{ loading, fetch, hasMore }">
<button
v-if="hasMore"
@click="fetch"
:disabled="loading"
>
{{ loading ? 'Cargando...' : 'Cargar más' }}
</button>
</template>
</InfiniteScroll>
</template>Modo Manual Después de N Páginas
Activa el modo manual automáticamente después de cierto número de páginas:
<template>
<!-- Automático las primeras 3 páginas, luego manual -->
<InfiniteScroll data="posts" :manual-after="3">
<template #next="{ loading, fetch, hasMore, manualMode }">
<button
v-if="manualMode && hasMore"
@click="fetch"
:disabled="loading"
>
Cargar más posts
</button>
</template>
<article v-for="post in posts.data" :key="post.id">
{{ post.title }}
</article>
</InfiniteScroll>
</template>Slots de Carga Personalizados
<template>
<InfiniteScroll data="posts">
<article v-for="post in posts.data" :key="post.id">
{{ post.title }}
</article>
<template #loading>
<div class="flex justify-center py-4">
<div class="spinner"></div>
<span>Cargando más posts...</span>
</div>
</template>
</InfiniteScroll>
</template>Resetear al Cambiar Filtros
Cuando cambias filtros, necesitas resetear los datos para evitar que se mezclen con los existentes:
<script setup>
import { router } from '@inertiajs/vue3'
import { InfiniteScroll } from '@inertiajs/vue3'
defineProps({
posts: Object,
currentCategory: String
})
const filterByCategory = (category) => {
router.visit(route('posts.index'), {
data: { category },
only: ['posts', 'currentCategory'],
reset: ['posts']
})
}
</script>
<template>
<div class="mb-4">
<button @click="filterByCategory('tech')">Tecnología</button>
<button @click="filterByCategory('news')">Noticias</button>
<button @click="filterByCategory(null)">Todos</button>
</div>
<InfiniteScroll data="posts">
<article v-for="post in posts.data" :key="post.id">
{{ post.title }}
</article>
</InfiniteScroll>
</template>Múltiples Contenedores de Scroll
Para tener varios infinite scroll independientes en una página:
<?php
namespace App\Http\Controllers;
use App\Models\User;
use App\Models\Order;
use Inertia\Inertia;
class DashboardController extends Controller
{
public function index()
{
return Inertia::render('Dashboard', [
'users' => Inertia::scroll(
fn () => User::paginate(pageName: 'users')
),
'orders' => Inertia::scroll(
fn () => Order::paginate(pageName: 'orders')
),
]);
}
}Esto genera URLs como ?users=2&orders=3 en lugar de conflictos con ?page=.
Acceso Programático
Para controlar el scroll infinito desde código:
<script setup>
import { ref } from 'vue'
import { InfiniteScroll } from '@inertiajs/vue3'
const scrollRef = ref(null)
const loadMore = () => {
scrollRef.value?.fetchNext()
}
const loadPrevious = () => {
scrollRef.value?.fetchPrevious()
}
</script>
<template>
<button @click="loadMore">Cargar siguiente página</button>
<InfiniteScroll ref="scrollRef" data="posts" manual>
<article v-for="post in posts.data" :key="post.id">
{{ post.title }}
</article>
</InfiniteScroll>
</template>Ejemplo Completo: Feed Social
Combinemos las tres características en un ejemplo práctico de feed social:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use App\Models\Notification;
use Inertia\Inertia;
class FeedController extends Controller
{
public function index()
{
return Inertia::render('Feed', [
'posts' => Inertia::scroll(fn () => Post::with(['author', 'likes'])
->latest()
->paginate(10)),
'notifications' => fn () => Notification::where('user_id', auth()->id())
->unread()
->count(),
'trending' => fn () => Post::trending()->take(5)->get(),
]);
}
}<script setup>
import { Link, InfiniteScroll } from '@inertiajs/vue3'
import { usePoll } from '@inertiajs/vue3'
defineProps({
posts: Object,
notifications: Number,
trending: Array
})
// Actualizar notificaciones cada 30 segundos
usePoll(30000, {
only: ['notifications']
})
</script>
<template>
<div class="max-w-4xl mx-auto">
<!-- Navegación con prefetching -->
<nav class="flex gap-4 mb-6">
<Link href="/feed" prefetch class="nav-link">
Inicio
</Link>
<Link href="/profile" prefetch="hover" class="nav-link">
Perfil
</Link>
<Link href="/notifications" prefetch class="nav-link">
Notificaciones
<span v-if="notifications > 0" class="badge">
{{ notifications }}
</span>
</Link>
</nav>
<div class="grid grid-cols-3 gap-6">
<!-- Feed principal con infinite scroll -->
<main class="col-span-2">
<InfiniteScroll data="posts" :buffer="300">
<article
v-for="post in posts.data"
:key="post.id"
class="post-card mb-4"
>
<div class="flex items-center gap-2 mb-2">
<img
:src="post.author.avatar"
class="w-10 h-10 rounded-full"
/>
<span class="font-bold">{{ post.author.name }}</span>
</div>
<p>{{ post.content }}</p>
<div class="flex gap-4 mt-2 text-gray-500">
<span>❤️ {{ post.likes.length }}</span>
<span>💬 {{ post.comments_count }}</span>
</div>
</article>
<template #loading>
<div class="text-center py-4">
Cargando más posts...
</div>
</template>
</InfiniteScroll>
</main>
<!-- Sidebar con trending (prefetch en hover) -->
<aside>
<h3 class="font-bold mb-4">Tendencias</h3>
<ul>
<li v-for="post in trending" :key="post.id">
<Link
:href="`/posts/${post.id}`"
prefetch
cache-for="5m"
>
{{ post.title }}
</Link>
</li>
</ul>
</aside>
</div>
</div>
</template>Conclusión
Inertia 2.0 ha transformado la forma de construir aplicaciones con Laravel y Vue. Las peticiones asíncronas nativas permiten implementar patrones de UX modernos sin la complejidad de gestionar APIs o WebSockets.
El prefetching mejora drásticamente la velocidad percibida, el polling mantiene los datos actualizados sin configuración compleja, y el infinite scroll proporciona una experiencia de navegación fluida para grandes conjuntos de datos.
Estas tres características trabajan perfectamente juntas, permitiendo crear aplicaciones que se sienten instantáneas y reactivas mientras mantienen la simplicidad del desarrollo server-side con Laravel.