Cómo implementar scroll infinito con Svelte y TanStack Query
El scroll infinito es una técnica muy utilizada en aplicaciones modernas para cargar contenido dinámico a medida que el usuario se desplaza. En este artículo, te mostraré cómo implementarlo usando Svelte, TanStack Query y un pequeño hook personalizado para detectar la intersección de elementos.
Contexto Del proyecto
La funcionalidad permite cargar una lista de lanzamientos musicales de manera progresiva, utilizando la librería TanStack Query para gestionar el estado de las peticiones. Además, empleamos un hook useInfiniteScroll
para detectar cuando el usuario llega al final del contenido visible.
Configuración De useInfiniteGetReleases
Este hook basado en TanStack Query realiza peticiones a una API para recuperar datos paginados. Configuramos el comportamiento del scroll infinito con los siguientes pasos clave:
- Función de query (
queryFn
): Solicita datos desde la API según el número de página. - Parámetro para la siguiente página (
getNextPageParam
): Define la lógica para determinar si hay más contenido disponible. - Selección de datos (
select
): Normaliza y simplifica la estructura de los datos antes de exponerlos al componente.
import { releaseQueryClient } from '$lib/global-query-client/release';
import { createInfiniteQuery } from '@tanstack/svelte-query';
const { getReleases } = releaseQueryClient();
export const useInfiniteGetReleases = (perPage?: number) =>
createInfiniteQuery({
queryKey: ['releases'],
queryFn: ({ pageParam }) => getReleases.queryFn(String(pageParam), perPage),
initialPageParam: 1,
getNextPageParam: ({ pagination }) => {
return pagination.urls.next ? pagination.page + 1 : undefined;
},
select: (data) => {
return {
pages: data.pages.map((page) => {
return {
releases: page.releases.map((release) => {
return {
title: release.basic_information.title,
artist: release.basic_information.artists[0].name,
cover_image: release.basic_information.cover_image,
id: release.id
};
})
};
}),
pageParams: data.pageParams
};
},
staleTime: 1000 * 60 * 10
});
Detectando El scroll infinito
El hook useInfiniteScroll
utiliza la API de IntersectionObserver
para ejecutar una acción cada vez que un elemento es visible en la pantalla. Es perfecto para detectar cuándo se debe cargar más contenido.
type Params = {
action: () => void;
element: HTMLElement;
};
export const useInfiniteScroll = () => (params: Params) => {
const { action, element } = params;
if (element) {
const observer = new IntersectionObserver(
(entries) => {
const first = entries[0];
if (first.isIntersecting) {
action();
}
},
{ threshold: 1 }
);
observer.observe(element);
}
};
Integración En el componente Svelte
El componente principal utiliza estos hooks para implementar el scroll infinito. Aquí está el código explicado paso a paso:
<script lang="ts">
import { useInfiniteGetReleases, useInfiniteScroll } from '$lib/hooks';
const query = useInfiniteGetReleases(12);
const infiniteScroll = useInfiniteScroll();
let elementRef: HTMLElement;
// Activar el scroll infinito cuando se cumplan las condiciones
$: {
if (
elementRef &&
$query.hasNextPage &&
!$query.isLoading &&
!$query.isFetchingNextPage &&
!$query.isFetching
) {
infiniteScroll({
action: () => {
setTimeout(() => $query.fetchNextPage(), 1500);
},
element: elementRef
});
}
}
</script>
<section class="space-y-2">
<!-- Renderizar la lista de lanzamientos -->
<ul class="grid grid-cols-12 gap-1 md:gap-2">
{#if $query.data}
{#each $query.data.pages as { releases }}
{#each releases as release}
<li class="col-span-4 md:col-span-3 lg:col-span-2">
<a href={`/release/${release.id}`} class="">
<img
class="aspect-square object-sm w-full rounded-sm object-cover"
src={release.cover_image}
alt={release.title + ' by ' + release.artist}
/>
</a>
</li>
{/each}
{/each}
{/if}
</ul>
<!-- Elemento para observar el scroll -->
<div bind:this={elementRef}>
{#if $query.isFetching && $query.hasNextPage}
<ul class="grid grid-cols-12 gap-1 md:gap-2">
{#each new Array(12) as _, i}
<li class="col-span-4 md:col-span-3 lg:col-span-2">
<div class="aspect-square w-full animate-pulse rounded-sm bg-neutral-300"></div>
</li>
{/each}
</ul>
{/if}
</div>
</section>
- Condiciones del scroll infinito: Observamos
elementRef
solo cuando hay más contenido disponible y las peticiones actuales han finalizado. - Elementos visuales: Mostramos un “loader” animado mientras se recuperan datos.
Conclusión
Esta solución combina la gestión de datos eficiente de TanStack Query con la reactividad de Svelte para crear un scroll infinito fluido. El uso de IntersectionObserver
asegura un rendimiento óptimo al manejar las actualizaciones del DOM solo cuando es necesario.
¡Implementa este patrón en tus proyectos y mejora la experiencia de usuario! 🚀