Vous souhaitez nous soutenir ? Devenez sponsor de l'association sur notre page Github

Inertia 2.0 : L'infinite scroll enfin simple

Publié le 16 octobre 2025 par ludovic guenet
Couverture de l'article Inertia 2.0 : L'infinite scroll enfin simple

Inertia 2.0 débarque avec une fonctionnalité que tout le monde attendait : l'infinite scroll natif. Fini les bibliothèques tierces, les calculs d'offset compliqués, et les heures perdues à synchroniser frontend et backend. Tout est maintenant intégré, simple, et ça fonctionne out of the box.

Voyons comment transformer une pagination classique en infinite scroll en quelques lignes de code.

Vous êtes un "visual learner" ? Regardez mon tutorial vidéo sur YouTube:

Le problème de la pagination infinie

Avant Inertia 2.0, mettre en place un infinite scroll était un vrai casse-tête. Il fallait :

  • Gérer les appels API manuellement
  • Calculer les offsets et les limites
  • Merger les nouvelles données avec les anciennes
  • Gérer l'état du scroll
  • Synchroniser l'URL avec la page active
  • Ne pas oublier le loading state

Bref, une feature qui paraît simple mais qui devient vite un cauchemar à maintenir. Et quand le client change d'avis et veut revenir à une pagination classique ? Bon courage pour tout défaire.

La solution Inertia : deux lignes de code

Avec Inertia 2.0, l'infinite scroll devient ridiculement simple. Regardez plutôt.

Côté backend : une façade, c'est tout

Dans votre contrôleur Laravel, remplacez votre retour classique par Inertia::scroll() :

1use Inertia\Inertia;
2 
3Route::get('/users', function () {
4 return Inertia::render('Users/Index', [
5 'users' => Inertia::scroll(fn () => User::paginate())
6 ]);
7});`

C'est tout. Vraiment. La méthode Inertia::scroll() configure automatiquement le comportement de merge et normalise les métadonnées de pagination pour le frontend.

Et ça fonctionne avec tous les types de pagination Laravel :

  • paginate() - La pagination classique avec offset
  • simplePaginate() - Sans le count total
  • cursorPaginate() - La pagination par curseur (plus performante)

Même avec les API Resources :

1'users' => Inertia::scroll(
2 UserResource::collection(User::paginate(20))
3)

Côté frontend : un composant qui fait le job

Maintenant, côté Vue (ou React, ou Svelte), enveloppez votre liste avec le composant <InfiniteScroll> :

1<template>
2 <InfiniteScroll data="users">
3 <div v-for="user in users.data" :key="user.id">
4 {{ user.name }}
5 </div>
6 </InfiniteScroll>
7</template>

Vous notez l'attribut data="users" ? C'est le nom de la prop paginée côté backend. C'est tout ce qu'il faut.

Le composant utilise l'Intersection Observer API pour détecter automatiquement quand l'utilisateur arrive en bas de la liste et charge la suite. Les nouvelles données sont mergées automatiquement avec les existantes.

Synchronisation d'URL automatique

Voici un truc génial : l'URL se met à jour automatiquement quand vous scrollez.

Vous descendez sur la page 2 ? L'URL devient ?page=2. Vous continuez sur la page 4 ? L'URL passe à ?page=4. Et ça fonctionne dans les deux sens : si vous remontez, l'URL redescend aussi.

Résultat : vous pouvez partager le lien d'une page spécifique ou bookmarker votre position. L'utilisateur reviendra exactement où il était.

Si vous ne voulez pas ce comportement (par exemple pour des commentaires secondaires), désactivez-le :

1<InfiniteScroll data="users" preserve-url>
2 <!-- ... -->
3</InfiniteScroll>

Le mode manuel : reprendre le contrôle

Parfois, vous ne voulez pas un scroll infini... infini. Imaginez une liste de milliers d'utilisateurs : vos visiteurs ne verront jamais le footer ! Inertia propose le mode manuel avec manual-after :

1<InfiniteScroll data="users" :manual-after="3">
2 <template #next="{ loading, fetch, hasMore }">
3 <button
4 v-if="hasMore"
5 @click="fetch"
6 :disabled="loading"
7 class="bg-blue-500 text-white px-4 py-2 mt-5"
8 >
9 {{ loading ? 'Chargement...' : 'Charger plus' }}
10 </button>
11 </template>
12 
13 <div v-for="user in users.data" :key="user.id">
14 {{ user.name }}
15 </div>
16</InfiniteScroll>

Après 3 pages chargées automatiquement, un bouton "Charger plus" apparaît. L'utilisateur reprend le contrôle et peut atteindre le footer. Les slots previous et next vous donnent accès à :

  • loading - État de chargement
  • fetch - Fonction pour charger plus
  • hasMore - Y a-t-il encore des données ?
  • manualMode - Le mode manuel est-il actif ?

Le chargement bidirectionnel

Par défaut, l'infinite scroll charge dans les deux directions. Si un utilisateur arrive sur la page 5 (via un lien partagé), il peut scroller vers le haut pour charger les pages 4, 3, 2, 1... et vers le bas pour charger 6, 7, 8... Vous pouvez contrôler ce comportement :

1<!-- Charger uniquement vers le bas -->
2<InfiniteScroll data="users" only-next>
3 <!-- ... -->
4</InfiniteScroll>
5 
6<!-- Charger uniquement vers le haut -->
7<InfiniteScroll data="messages" only-previous>
8 <!-- ... -->
9</InfiniteScroll>

Le mode reverse pour les chats

Pour les applications de messagerie où les messages récents sont en bas, activez le mode reverse :

1<InfiniteScroll data="messages" reverse>
2 <div v-for="message in messages.data" :key="message.id">
3 {{ message.content }}
4 </div>
5</InfiniteScroll>

En mode reverse :

  • Scroller vers le haut charge les messages plus anciens (next page)
  • Scroller vers le bas charge les messages plus récents (previous page)
  • Le scroll automatique positionne en bas au chargement initial

C'est vous qui inversez l'ordre d'affichage des messages, Inertia gère le reste.

Réinitialiser lors des filtres

Quand l'utilisateur change un filtre, vous devez réinitialiser les données au lieu de les merger :

1<script setup>
2import { router } from '@inertiajs/vue3'
3 
4const show = (role) => {
5 router.visit(route('users'), {
6 data: { filter: { role } },
7 only: ['users'],
8 reset: ['users'], // ← La magie est ici
9 })
10}
11</script>
12 
13<template>
14 <button @click="show('admin')">Admins</button>
15 <button @click="show('customer')">Clients</button>
16 
17 <InfiniteScroll data="users">
18 <div v-for="user in users.data" :key="user.id">
19 {{ user.name }}
20 </div>
21 </InfiniteScroll>
22</template>

L'option reset: ['users'] dit à Inertia de repartir de zéro au lieu d'ajouter à la liste existante.

Personnalisation avancée

Buffer de chargement

Contrôlez à quel moment le chargement démarre :

1<InfiniteScroll data="users" :buffer="500">
2 <!-- Charge 500px avant la fin -->
3</InfiniteScroll>

Un buffer plus grand améliore l'expérience (pas d'attente visible) mais charge potentiellement du contenu jamais consulté.

Tableaux et éléments complexes

Quand vos données ne sont pas des enfants directs du composant, spécifiez l'élément cible :

1<InfiniteScroll
2 data="users"
3 items-element="#table-body"
4 start-element="#table-header"
5 end-element="#table-footer"
6>
7 <table>
8 <thead id="table-header">
9 <tr><th>Nom</th></tr>
10 </thead>
11 <tbody id="table-body">
12 <tr v-for="user in users.data" :key="user.id">
13 <td>{{ user.name }}</td>
14 </tr>
15 </tbody>
16 <tfoot id="table-footer">
17 <tr><td>Total: {{ users.total }}</td></tr>
18 </tfoot>
19 </table>
20</InfiniteScroll>

Ou avec des refs Vue (plus propre) :

1<script setup>
2import { ref } from 'vue'
3const tableBody = ref()
4const tableHeader = ref()
5const tableFooter = ref()
6</script>
7 
8<template>
9 <InfiniteScroll
10 data="users"
11 :items-element="() => tableBody"
12 :start-element="() => tableHeader"
13 :end-element="() => tableFooter"
14 >
15 <table>
16 <thead ref="tableHeader">
17 <tr><th>Nom</th></tr>
18 </thead>
19 <tbody ref="tableBody">
20 <tr v-for="user in users.data" :key="user.id">
21 <td>{{ user.name }}</td>
22 </tr>
23 </tbody>
24 <tfoot ref="tableFooter">
25 <tr><td>Footer</td></tr>
26 </tfoot>
27 </table>
28 </InfiniteScroll>
29</template>

Conteneurs scrollables

Ça fonctionne dans n'importe quel conteneur avec scroll, pas seulement le document principal :

1<div style="height: 400px; overflow-y: auto;">
2 <InfiniteScroll data="users">
3 <div v-for="user in users.data" :key="user.id">
4 {{ user.name }}
5 </div>
6 </InfiniteScroll>
7</div>

Le composant détecte automatiquement le conteneur scrollable parent.

Plusieurs scrolls sur la même page

Si vous avez plusieurs infinite scrolls, utilisez des pageName différents pour éviter les conflits d'URL :

1Route::get('/dashboard', function () {
2 return Inertia::render('Dashboard', [
3 'users' => Inertia::scroll(
4 fn () => User::paginate(pageName: 'users')
5 ),
6 'orders' => Inertia::scroll(
7 fn () => Order::paginate(pageName: 'orders')
8 ),
9 ]);
10});

Résultat : ?users=2&orders=3 au lieu de conflits sur ?page=.

Accès programmatique

Besoin de déclencher le chargement manuellement ? Utilisez une ref :

1<script setup>
2import { ref } from 'vue'
3const infiniteScrollRef = ref(null)
4 
5const fetchNext = () => {
6 infiniteScrollRef.value?.fetchNext()
7}
8</script>
9 
10<template>
11 <button @click="fetchNext">Charger plus</button>
12 
13 <InfiniteScroll ref="infiniteScrollRef" data="users" manual>
14 <!-- ... -->
15 </InfiniteScroll>
16</template>

Méthodes disponibles :

  • fetchNext() - Charger la page suivante
  • fetchPrevious() - Charger la page précédente
  • hasNext() - Y a-t-il une page suivante ?
  • hasPrevious() - Y a-t-il une page précédente ?

Intégration avec d'autres librairies

Si vous n'utilisez pas les paginateurs Laravel, vous pouvez fournir vos propres métadonnées :

1use Inertia\Contracts\ProvidesScrollMetadata;
2 
3class CustomScrollMetadata implements ProvidesScrollMetadata
4{
5 public function __construct(protected $data) {}
6 
7 public function getPageName(): string {
8 return 'page';
9 }
10 
11 public function getPreviousPage(): int|string|null {
12 return $this->data->previous_page;
13 }
14 
15 public function getNextPage(): int|string|null {
16 return $this->data->next_page;
17 }
18 
19 public function getCurrentPage(): int|string|null {
20 return $this->data->current_page;
21 }
22}
23 
24// Utilisation
25Inertia::scroll(
26 $customPaginatedData,
27 metadata: fn ($data) => new CustomScrollMetadata($data)
28);

Définissez même une macro pour éviter la répétition :

1// Dans AppServiceProvider
2Inertia::macro('customScroll', function ($data) {
3 return Inertia::scroll(
4 $data,
5 metadata: fn ($data) => new CustomScrollMetadata($data)
6 );
7});
8 
9// Dans vos contrôleurs
10return Inertia::render('Users/Index', [
11 'users' => Inertia::customScroll($customData)
12]);

Conclusion

L'infinite scroll d'Inertia 2.0 est exactement ce qu'on attendait : simple, puissant, et invisible. Plus besoin de bibliothèques tierces, de state management complexe, ou de calculs d'offset. Tout est géré nativement, de la synchronisation d'URL au chargement bidirectionnel.

C'est une de ces features qui paraît simple en surface mais qui cache une complexité énorme. Et c'est justement ça qui est génial : toute cette complexité est abstraite. Vous vous concentrez sur votre logique métier, Inertia gère le reste.

Deux lignes de code pour passer d'une pagination classique à un infinite scroll. C'est ça, la magie d'Inertia.

ludovic guenet avatar
ludovic guenet
software engineer • mentor • bassist

A lire

Autres articles de la même catégorie