Índice de contenido
- ¿Qué devuelve Laravel al paginar con Inertia?
- Backend: preparar paginación en Laravel
- Frontend con Vue 3 + Inertia
- Iterar correctamente los links
- Evitar enlaces a la página actual
-
Render dinámico con
: Crear un componente Pagination.vue reutilizable con Tailwind 4 - Infinite Scroll en Laravel Inertia (oficial)
- Backend: Inertia::scroll()
- Modos avanzados
- Paginación clásica vs Infinite Scroll
- Preguntas frecuentes
- Conclusión
Cuando trabajas con Laravel + Inertia + Vue 3, tarde o temprano llegas al mismo punto: tienes un listado grande y necesitas paginación. Inertia no trae un componente de paginación “listo para usar”, pero sí te da todas las piezas necesarias para construir una solución limpia, reutilizable y moderna.
En esta guía te muestro cómo implementar paginación en Laravel Inertia, primero de forma clásica y luego dando el salto a Infinite Scroll, usando la API oficial de Inertia. Es exactamente el flujo que yo mismo seguí al construir listados reales en paneles de administración y dashboards.
¿Qué devuelve Laravel al paginar con Inertia?
Cuando usas cualquiera de los métodos de paginación de Laravel (paginate, simplePaginate, cursorPaginate) y los envías a Inertia, el framework serializa automáticamente la información necesaria para el frontend.
Un paginado típico incluye:
- data: los registros de la página actual
- links: los enlaces de navegación
- meta: información adicional (página actual, total, etc.)
Por ejemplo, desde un controlador:
public function index()
{
return Inertia::render('Dashboard/Category/Index', [
'categories' => Category::paginate(10)
]);
}En el frontend recibes algo como:
categories: {
data: [...],
links: [
{ url: null, label: '« Previous', active: false },
{ url: 'http://app.test?page=1', label: '1', active: true },
{ url: 'http://app.test?page=2', label: '2', active: false },
...
]
}Aquí es donde empieza el verdadero trabajo en Vue o React.
En Inertia, no tenemos un componente de paginación para usar desde el cliente (Vue o React); así que, tenemos que crear uno; recordemos que nos quedamos en esta guía en el uso de los mensajes flash en Laravel Inertia.
Backend: preparar paginación en Laravel
En el backend no hay mucho misterio, pero sí buenas prácticas:
- Paginar siempre en el servidor
- Evitar traer colecciones completas
- Mantener consistencia en filtros y orden
Un ejemplo más realista:
public function index(Request $request)
{
$categories = Category::orderBy('created_at', 'desc')
->paginate(10)
->withQueryString();
return Inertia::render('Dashboard/Category/Index', [
'categories' => $categories
]);
}- withQueryString() es clave si usas filtros o búsquedas: evita que se pierdan al cambiar de página.
Desde el listado paginado, debemos el apartado de links en los cuales se encuentran todos los enlaces para la paginación en un array:
Frontend con Vue 3 + Inertia
Recibir los datos paginados
En tu página:
<script setup>
defineProps({
categories: Object
})
</script>Y ya puedes usar categories.data y categories.links.
Iterar correctamente los links
El array links ya viene listo para renderizar, así que basta con iterarlo:
<Link
v-for="l in categories.links"
:key="l.label"
:href="l.url"
v-html="l.label"
/>Pero aquí aparece el primer problema real…
Evitar enlaces a la página actual
Cuando active === true, no tiene sentido renderizar un enlace que navegue a sí mismo. En mi caso, lo resolví separando <Link> y <span>:
<template v-for="l in categories.links" :key="l.label">
<Link
v-if="!l.active"
class="px-2 py-1"
:href="l.url"
v-html="l.label"
/>
<span
v-else
class="px-2 py-1 cursor-pointer text-gray-500"
v-html="l.label"
/>
</template>Funciona, pero se puede limpiar mucho más.
Render dinámico con <Component>: Crear un componente Pagination.vue reutilizable con Tailwind 4
Desde Vue, tenemos una etiqueta llamada "Component" con la cual podemos renderizar componentes de manera dinámica en base a una condición evaluada en con el prop de is:
import Foo from './Foo.vue'
import Bar from './Bar.vue'
export default {
components: { Foo, Bar },
data() {
return {
view: 'Foo'
}
}
}
</script>
<template>
<component :is="view" />
</template>Vue tiene el componente <Component> que permite renderizar dinámicamente HTML o componentes según una condición.
Con este componente en Vue, podemos renderizar ya sea componente de Vue o HTML directamente desde la definición del mismo:
<Component
v-for="l in categories.links"
:key="l.label"
:is="!l.active ? 'Link' : 'span'"
class="px-2 py-1"
:class="!l.active ? '' : 'text-gray-500 cursor-pointer'"
:href="l.url"
v-html="l.label"
/>Y con esto, tenemos el mismo resultado que antes, pero queda más limpia la sintaxis, como puedes ver, podemos agregar Tailwind 4 en cualquier momento.
Ahora, para poder reutilizar el componente de manera global, creamos un nuevo componente que reciba como parámetro el listado paginado mediante un prop y maquetado con Tailwind 4:
resources/js/Shared/Pagination.vue
<template>
<!-- <Link class="px-2 py-1" v-if="!l.active" :href="l.url">{{ l.label }}</Link>
<span class="px-2 py-1 cursor-pointer text-gray-500" v-else>{{ l.label }}</span> -->
<template v-for="l in links" v-bind:key="l">
<Component v-html="`${l.label}`" class="px-2 py-1" :is="!l.active ? 'Link' : 'span'"
:href="l.url == null ? '#' : l.url" :class="!l.active ? '' : 'cursor-pointer text-gray-500'"
/>
</template>
</template>
<script>
import { Link } from '@inertiajs/vue3';
export default {
components: {
Link
},
props: {
links: Array
}
}
</script>Para desactivar la página actual y evitar dejar un enlace que navegue a sí mismo, podemos hacer un condicional y colocar un SPAN cuando se utilice la página actual:
<template v-for="l in categories.links" :key="l">
<Link v-if="!l.active" class="px-2 py-1" :href="l.url" v-html="l.label"/>
<span v-else class="px-2 py-1" v-html="l.label" />
</template>Y agregar algo de estilo para indicar que es un enlace desactivado, aunque realmente es una etiqueta SPAN:
<template v-for="l in categories.links" :key="l">
<Link v-if="!l.active" class="px-2 py-1" :href="l.url" v-html="l.label"/>
<span v-else class="px-2 py-1 cursor-pointer text-gray-500" v-html="l.label" />
</template>
<script>Cuando ya tienes esto funcionando, lo lógico es reutilizarlo. Yo suelo crear un componente compartido.
resources/js/Shared/Pagination.vue
<script setup>
import { Link } from '@inertiajs/vue3'
defineProps({
links: Array
})
</script>
<template>
<template v-for="l in links" :key="l.label">
<Component
:is="!l.active ? 'Link' : 'span'"
class="px-2 py-1"
:class="!l.active ? '' : 'cursor-pointer text-gray-500'"
:href="l.url ?? '#'"
v-html="l.label"
/>
</template>
</template>Luego lo usas así:
<Pagination :links="categories.links" />Y listo. Paginación limpia, consistente y reutilizable.
Desde el listado de categorías, usamos este componente:
resources/js/Pages/Dashboard/Category/Index.vue
***
<pagination :links="categories.links" />
</app-layout>
</template>
***
import Pagination from '@/Shared/Pagination.vue'
export default {
components:{
AppLayout,
Link,
Pagination
},
// ***Y obtendremos:
http://localhost/category?page=1
Infinite Scroll en Laravel Inertia (oficial)
Cuando la paginación clásica se queda corta (feeds, chats, grids), Infinite Scroll es la mejor opción.
Cuándo usar Infinite Scroll
- Listados largos
- Feeds tipo redes sociales
- Chats
- Galerías o grids
- Productos relacionados
No lo uso para todo, pero cuando encaja, la experiencia de usuario mejora muchísimo.
Backend: Inertia::scroll()
Inertia ofrece un método específico para esto:
Route::get('/users', function () {
return Inertia::render('Users/Index', [
'users' => Inertia::scroll(fn () => User::paginate())
]);
});Este método:
Configura automáticamente el merge de datos
- Normaliza la metadata
- Funciona con paginate, simplePaginate, cursorPaginate
- Funciona con API Resources
- Frontend: <InfiniteScroll />
En Vue:
<script setup>
import { InfiniteScroll } from '@inertiajs/vue3'
defineProps(['users'])
</script>
<template>
<InfiniteScroll data="users">
<div v-for="user in users.data" :key="user.id">
{{ user.name }}
</div>
</InfiniteScroll>
</template>Internamente usa Intersection Observer y va cargando páginas sin reemplazar el contenido.
Buffer, URL sync y reset
Puedes controlar cuándo se cargan los datos:
<InfiniteScroll data="users" :buffer="500">También puedes evitar que la URL cambie:
<InfiniteScroll data="users" preserve-url>Y cuando aplicas filtros, es importante resetear:
router.visit(route('users'), {
data: { filter: { role } },
only: ['users'],
reset: ['users'],
})Esto evita que los nuevos resultados se mezclen con los anteriores.
Modos avanzados
Solo siguiente página:
<InfiniteScroll data="users" only-next />Modo reverse (ideal para chats):
<InfiniteScroll data="messages" reverse />También puedes activar modo manual y controlar tú cuándo cargar más contenido.
Paginación clásica vs Infinite Scroll
- Caso Mejor opción
- Panel admin Paginación clásica
- Feed social Infinite Scroll
- Chat Infinite Scroll (reverse)
- SEO estricto Paginación clásica
- UX moderna Infinite Scroll
Preguntas frecuentes
- ¿Infinite Scroll afecta al SEO?
- No, Inertia mantiene sincronizada la URL con ?page=.
- ¿Puedo combinar paginación e infinite scroll?
- Sí, en diferentes secciones del mismo proyecto.
- ¿Cuál es más fácil de mantener?
- La paginación clásica. Infinite Scroll es más potente, pero también más compleja.
Conclusión
La paginación en Laravel Inertia no es un problema: es una oportunidad para construir una experiencia sólida y reutilizable. Empieza con paginación clásica, crea tus componentes y, cuando el proyecto lo pida, da el salto a Infinite Scroll usando la API oficial.
Es exactamente el camino que yo recomiendo y el que mejor escala en proyectos reales.
Siguiente paso, conozcamos el uso de otro componente que a diferencia de este ya existe en Inertia, la Barra de progreso y spinner.