Paginación: todo lo que necesitas saber

Lista de técnicas para implementar la paginación de elementos en tus aplicaciones

Image for post
Image for post

Cuando en un proyecto nos piden que paginemos los elementos de una lista generalmente no solemos tardar mucho, ¿verdad? Lo primero que hacemos es escribir una consulta de este estilo:

Y, a continuación, diseñar una UX que a todos nos sonará:

Image for post
Image for post
Páginas numeradas

Sin embargo, tengo una mala noticia: este tipo de paginación no es tan válido en un mundo plagado de scrolls infinitos y actividad en tiempo real. Por tanto, a lo largo de este artículo explicaré por qué a veces este método nos puede traer problemas y los sistemas alternativos que podemos emplear para evitarlos.

Paginar con limit, offset: quizás una mala idea

Supongamos que tenemos una lista de fotos (al más puro estilo Instagram) que queremos mostrar de manera paginada con las más recientes al principio y usando infinite scroll. Seguramente la solución que se nos venga a la cabeza para mostrar la primera página pase por una consulta de este tipo:

La cual nos devolverá las 10 primeras imágenes más recientes. Hasta aquí todo bien.

Ahora supongamos que el usuario baja hasta el final de modo que tenemos que cargar las 10 siguientes imágenes. Nuestra siguiente consulta será:

Obteniendo las 10 siguientes fotos.

¿Qué sucede si entre medias un usuario B añadió una nueva foto? Pues que el usuario inicial verá una imagen duplicada debido a que la nueva foto introducida por el usuario B empujará la décima foto a la posición undécima de modo que pasará a formar parte de la segunda página:

Image for post
Image for post

De hecho la situación sería incluso peor si se pueden eliminar elementos de la lista. Si el usuario B elimina un elemento de la primera página y el usuario A solicita a continuación la segunda página el elemento undécimo no será visible pues la paginación lo saltaría. Imaginad que esto sucede en una aplicación de mensajería: desastre al canto.

Así que, cuando nos veamos tentados de emplear este tipo de paginación deberemos de valorar tanto la probabilidad de que suceda un caso de estos casos como la gravedad que supone para nuestra aplicación que suceda. Por ejemplo, quizás para en un blog básico sea muy improbable que suceda y en el caso de darse no sería muy dramático, por lo que podemos beneficiarnos de la rapidez en que se implementa este sistema.

Sin embargo, si no podemos asumir estos inconvenientes, deberemos de implementar algún sistema de paginación estable.

Paginación estable

El problema anterior se debe a que el método anterior de paginación depende de los valores volátiles limit y offset lo cual lo hacen inestable en casos como el anterior. Una posible solución pasaría por escoger un valor que sea estable a lo largo del tiempo y emplearlo para paginar a través de él. En nuestro ejemplo de las fotos podría ser la fecha de creación de la foto.

A partir de ahí, cuando queramos recorrer las páginas, en vez de pasar a la consulta el número de página que queremos extraer, pasaremos la fecha de la último foto que se ha devuelto:

De este modo estaremos seguros de que el usuario siempre verá el conjunto de fotos posteriores a la que última que vio…

Salvo que dos fotos tengan la misma fecha.

Una forma de resolver este problema sería modificar la consulta para obtener todas las fotos con fecha igual o anterior a la que recibamos y luego filtrar las repetidas (ñapa del 15 y lo sabes).

Y la otra, sería emplear el id de la foto (siempre que sea incremental) como elemento sobre el que paginar, lo cual nos aseguraría no tener elementos repetidos tras la consulta.

Nota. Por si no os habíais dado cuenta, uno de los inconvenientes que tiene este método (así como el que describiré a continuación) es que no vamos a poder tener paginadores de este estilo:

[<<] [<] [1] 2 [3] ... [>][>>]

No obstante, creo que es asumible prescindir de la posibilidad de saltar entre páginas, especialmente si nos movemos hacia una paginación por scroll infinito en la que el usuario no tiene esa opción y que además parece ser la tendencia, especialmente en móviles.

Este tipo de paginación se denomina paginación basada en cursor y es en la que se va a centrar el resto del artículo.

Paginación basada en cursor

Si observáis este diagrama:

Image for post
Image for post

El problema de mostrar elementos duplicados/perderlos cuando se añade/se eliminan elementos quedaría resuelto si pudiéramos especificar directamente a partir de qué elemento queremos obtener los siguientes resultados. De este modo, no importaría cuantos elementos se añadiesen al comienzo de la lista ya que nosotros dispondríamos de un cursor estable apuntando exactamente al lugar desde donde queremos seguir obteniendo resultados.

Este tipo de paginación se basa en un elemento denominado cursor que es una especie de identificador el cual representa una ubicación dentro de una lista paginada.

De este modo, cuando queramos obtener datos, necesitaremos dos parámetros:

  • El cursor desde donde empezar
  • Y el número de datos que queremos obtener.

Como por ejemplo:

En este caso, el parámetro after indicaría el cursor desde donde queremos seguir obteniendo datos y first haría las veces de LIMIT , es decir, el número de resultados que queremos obtener.

Como hemos visto en la introducción a la paginación estable, la forma más sencilla de implementar este método sería escoger un cursor estable a lo largo del tiempo y extraer los datos mediante consultas de este tipo:

Para posteriormente devolver los resultados de este modo (en el caso de que por ejemplo se tratase de una API REST):

Aquí, la propiedad post contendría el array de posts, mientras que la propiedad cursors contendría una propiedad llamada after que especificaría el valor del cursor que tendremos que pasar a la API en la siguiente llamada para obtener la siguiente tanda de resultados.

Si habéis navegado alguna vez por Twitter (quién no…), el feed se va rellenando conforme la gente a la que seguimos publica sus tweets, por lo que es claro que deben emplear un sistema de paginación similar al de cursores. De hecho, para realizar búsquedas de tweets a través de su API, las llamadas tienen esta forma:

Es decir, empleamos el parámetro count para delimitar el número de resultados que queremos y el parámetro since_id para especificar a partir de que tweet queremos obtener los resultados. Para esa consulta, el resultado devuelto incluye el campo search_metadata con la siguiente información:

En la que contamos con la url para obtener la siguiente página ( next_results ) así como el max_id de los resultados devueltos. La llamada completa podéis verla en este enlace.

Relay cursor connections

Con la llegada de GraphQL, apareció una especificación genérica para realizar este trabajo con cursores implementada por la librería Relay de Facebook: los Relay Cursor Connections:

Esta especificación es verdaderamente útil ya que se encarga de generalizar el concepto de cursor y establece un estándar para llevar a cabo las peticiones paginadas y el formato de las respuestas. Para que os hagáis una idea del aspecto propuesto por esta especificación aquí tenéis el aspecto de una query en GraphQL:

Las principales características de este formato son las siguientes:

  • uso de la clave pageInfo para obtener información acerca de la página actual como por ejemplo si hay más páginas. Sin embargo, no se devuelve el número total de elementos ya que teóricamente los clientes de Relay no necesitan esa información
  • uso de la clave edges para obtener el array de resultados, conocidos como nodos
  • cada resultado posee por un lado la información del nodo y por el otro su propio cursor, es decir, cada elemento es devuelto junto con su propio cursor de cara a facilitar la navegación por páginas (por ejemplo, podríamos realizar la siguiente consulta partiendo desde el medio de la que tenemos gracias a que conocemos el valor del cursor del elemento en el centro).

Finalmente, pese a que esta especificación surgió para trabajar con GraphQL no necesariamente tenemos que ceñirnos a este lenguaje por lo que podemos portarla a una API Rest fácilmente. Es decir:

Relay tan solo generaliza el concepto de cursor y nos presenta un contrato entre cliente y servidor acerca del modo en que se paginan los resultados.

Conclusiones

Tras analizar los distintos métodos de paginación mi opinión sobre su uso es que deberíamos siempre escoger el que mejor se adapte a la aplicación o API que estemos desarrollando. Las principales diferencias serían en mi opinión:

  • En la paginación por offset la ordenación podemos realizarla a través de cualquier columna mientras que en la paginación por cursor los resultados son ordenados siempre en función del campo empleado para crear el cursor.
  • La paginación por offset contiene tanto los números de páginas como los enlaces al anterior y al siguiente mientras que en la paginación mediante cursores no es posible saltar entre páginas debido a la naturaleza dinámica de los datos a devolver.
  • La paginación por offset permite la navegación es ambos sentidos (la dirección es la misma 🤓 ) mientras que en la navegación por cursores lo más habitual es emplearla para ir hacia delante (pese a que también se puede implementar para permitir ir hacia atrás).

Así que puede que para comenzar cualquier proyecto sencillo nos baste con adoptar el sistema basado en números de página ( LIMIT y OFFSET ) pues la rapidez en implementarlo es un claro punto a su favor. Sin embargo, para aplicaciones más complejas que involucren mucho dato en tiempo real sí que creo que la paginación mediante cursores es una mejor opción, no en vano es la que emplean aplicaciones como Twitter y Facebook.

¿Quieres recibir más artículos como este?

Suscríbete a nuestra newsletter:

Written by

Entre paseo y paseo con Simba desarrollo en Symfony y React

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store