Paginacion Full-Stack con node.js para cualquier base de datos y cualquier FrontEnd framework [Ejemplos]

Santiago Quinteros - CEO & CTO - Software on the road
By:
Santiago Quinteros

Estás enviando todos tus datos guardados al frontend, ralentizando el rendimiento de tu aplicación?

Preocupado por el descenso de rendimiento que sufrirás en cuanto empiece a crecer la base de usuarios?

No busques más! Tus problemas se han ido.

No, no voy a venderte un paquete de npm al azar o algo así, en cambio, voy a enseñarte cómo funciona la paginación, y cómo puedes implementar para tu backend.

Tabla de contenidos

Pero qué es la paginación de todos modos?

Imaginemos que la tabla de la base de datos (o colecciones) es una hoja de cálculo de Excel.

Cada registro está en una fila que tiene un particular número y un particular orden

Así que, la paginación es cuando quieres sólo un trozo (o una página) de filas al mismo tiempo, digamos 20 por página.

Concepto de paginación

En la primera página, queremos seleccionar desde la posición inicial 0 hasta el 20º elemento.

Luego, en la segunda página, queremos seleccionar los elementos desde la posición 20 hasta la posición 40.

Y así sucesivamente.

Implementación - Lado del servidor

Por lo que sé, hay dos maneras de implementar la paginación, dejando que el cliente decida el tamaño de la página, o no permitiéndolo.

Opción 1 - El cliente decide el tamaño de la página

Utilice este método cuando tenga una API que pueda ser consumida por diferentes clientes, como una aplicación web y una aplicación móvil, así cada equipo de desarrollo puede decidir lo que mejor se adapte a sus necesidades

Para este enfoque, es útil imaginar el concepto de paginación como si estuviéramos en una hoja de cálculo de Excel, de esa manera, las variables que enviaremos tienen un poco más de sentido.

La primera es el "límite" que representa el tamaño de página deseado.

Luego la segunda sería el "salto", que significa cuántas filas debe "saltar" la base de datos.

// Un ejemplo de una función de controlador para Express.js
async getAllUser (req, res) {
    try {
      const limit = parseInt(req.query.limit); // Asegúrate de parsear el límite a número
      const skip = parseInt(req.query.skip);// Asegúrate de parsear el salto a número

      // Estamos usando la arquitectura de "3 capas" explorada en la "arquitectura a prueba de balas del node.js
      // Básicamente, es sólo una clase donde tenemos nuestra lógica de negocios
      const userService = new userService();
      const users = await userService.getAll(limit, skip);

      return res.status(200).json(users);
    } catch(e){
      return res.status(500).json(e)
    }
},

Entonces llamamos a la base de datos con Mongoose (o también se puede hacer con el driver nativo de MongoDB)

class UserPaginationExample {
    getAll(limit = 0, skip = 0) {
        return UsersModel.find({})  // Puede que quieras añadir una consulta
                        .skip(skip) // Siempre aplicar "salto" antes de "límite
                        .limit(limit) // Este es el "tamaño de la página"
    }
}

Otro ejemplo que utiliza el framework aggregation de MongoDB

class UserPaginationExample {
    getAll(limit = 0, skip = 0) {
        return UsersModel.aggregate([
            { $match: {} },    // Esta es tu consulta
            { $skip: skip },   // Siempre aplica "salto" antes de "límite
            { $limit: limit }, // Este es el "tamaño de la página"
        ])  
    }
}

Opción 2 - El servidor decide el tamaño de la página

Para este método, el cliente sólo envía el número de página que quiere, y confía en que el servidor entrega el tamaño de página correcto.

Dentro del servidor, todavía tienes "limite" y "salto" para uso interno, y el proceso es más o menos el mismo que antes.

// Un ejemplo de función de un controlador para Express.js
async getAllUser (req, res) {
    try {
      const page = parseInt(req.query.page); // Asegúrate de parsear la página a número
      // Estamos usando la arquitectura de "3 capas" explorada en la arquitectura a prueba de balas de node.js "
      // Básicamente, es sólo una clase donde tenemos nuestra lógica de negocios
      const userService = new userService();
      const users = await userService.getAll(page);
      return res.status(200).json(users);
    } catch(e){
      return res.status(500).json(e)
    }
},

Luego, al llamar a la base de datos con Mongoose (o también se puede hacer con el driver nativo de MongoDB)

class UserPaginationExample {
    getAll(page = 1) {
        const PAGE_SIZE = 20;                       // Similar a 'límite'
        const skip = (page - 1) * PAGE_SIZE;        // Para la página 1, el salto es: (1 - 1) * 20 => 0 * 20 = 0
        return UsersModel.find({})  
                        .skip(skip)                 // Igual que antes, se usa siempre 'omitir' primero
                        .limit(PAGE_SIZE)
    }
}

Otro ejemplo que utiliza el framwork de aggregation de MongoDB

class UserPaginationExample {
    getAll(page = 1) {
        const PAGE_SIZE = 20;                   // Similar a 'límite'
        const skip = (page - 1) * PAGE_SIZE;    // Para la página 1, el salto es: (1 - 1) * 20 => 0 * 20 = 0
        return UsersModel.aggregate([
            { $match: {} },
            { $skip: (page - 1) * PAGE_SIZE },
            { $limit: PAGE_SIZE },
        ])  
    }
}

Utilizo este método cuando no quiero no molestar a mi equipo de Frontend con definiciones complejas de API.

Implementación - Lado del cliente

El código de cliente para la paginación es bastante fácil de hacer, pero lo incluí de todas formas para que puedas copiarlo y empezar a usarlo de inmediato!

React

Usando Hooks

import React, { useState, useEffect } from 'react'

const fetchUsers = (limit, skip) => {
    // Asegúrate de enviar "limite" y "salto" como parámetros de consulta a tu servidor node.js
    fetch(`/api/users?limit=${limit}&skip=${skip}`) 
        .then((res) => {
            this.setState({
                users: res.data;
            })
        })
}

const userList = () => {

    const [users, setUsers] = useState([]);
    const [limit, setLimit] = useState(20);
    const [skip, setSkip] = useState(0);

    const nextPage = () => {
        setSkip(skip + limit)
    }

    const previousPage = () => {
        setSkip(skip - limit)
    }

    useEffect(() => {
        fetchUsers(limit, skip)
    }, [skip, limit])


    return (<div> 
        <div> 
            { 
                users.map(user => 
                    <div> 
                        <span> { user.name } </span>
                        <span> { user.email } </span>
                        <span> { user.lastLogin } </span>
                    </div>
                )
            }
        </div>
        <div> 
            <div onClick={nextPage}> Previous Page </div>
            <div onClick={previousPage}> Next Page </div> 
        </div>
    </div>)
}

Usando componentes de clase

import React from 'react';
class UsersList extends React.component {
    constructor(super){
        super();
        this.state = {
            users: [],
            // Valores iniciales para obtener la primera página
            limit: 20,  
            skip: 0,
        }
    }
    componendDidMount() {
        this.fetchUsers();
    }

    fetchUsers() {
        // Asegúrate de enviar "limite" y "salto" como parámetros de consulta a tu servidor node.js
        fetch(`/api/users?limit=${this.state.limit}&skip=${this.state.skip}`) 
            .then((res) => {
                this.setState({
                    users: res.data;
                })
            })
    }
    nextPage() {
        this.setState({
            skip: this.state.skip + this.state.limit,
        })
    }
    previousPage() {
        if(this.state.skip > 0) {
            this.setState({
                skip: this.state.skip - this.state.limit,
            })
        }
    }

    componentDidUpdate(prevProps, prevState) {
        // Intenta evitar hacer esto, es bastante fácil estropear las cosas con el ciclo de vida
        // Así que en vez de eso, aprende a usar react hooks de reacción, puedes leer mi guía de hooks aquí: https://softwareontheroad.com/react-hooks/
        //
    }


    render() {
        return (<div> 
            <div> 
                { 
                    this.state.users.map(user => 
                        <div> 
                            <span> { user.name } </span>
                            <span> { user.email } </span>
                            <span> { user.lastLogin } </span>
                        </div>
                    )
                }
            </div>
            <div> 
                <div onClick={this.nextPage}> Previous Page </div>
                <div onClick={this.previousPage}> Next Page </div> 
            </div>
        </div>)
    }
}

Vue

(Nota al margen: Este es mi framework/biblioteca favorito, es tan fácil de usar, y tan fácil de añadir a cualquier proyecto, me encanta.)

Enfoque 1: el cliente controla el tamaño de la página

<script>
const usersList = new Vue({
    el: '#user-list'
    data: {
        users: [],
        limit: 20,
        skip:  0,
    },
    methods: {
        nextPage() {
            this.skip += this.limit; // Para la siguiente página sólo tienes que incrementar "salto" para el tamaño de la página "límite".
            this.fetchPage();
        },
        previousPage() {
            if(skip > 0) {
                this.skip -= this.limit; // Para la página anterior, sólo tienes que decrementar el "salto" para el "límite" del tamaño de la página.
                this.fetchPage();
            }
        },
        fetchPage() {
            return fetch(`/api/users?limit=${this.limit}&skip=${this.skip}`) // Envía "limite" y "salto" como parámetros querty a tu servidor node.js
                .then((res) => {
                    this.users = res.data;
                })
        },
    },
    mounted() {
        this.fetchPage();
    },
})
</script>

Enfoque 2: el servidor controla el tamaño de la página

<script>
const usersList = new Vue({
    el: '#user-list'
    data: {
        users: [],
        page: 1,
    },
    methods: {
        nextPage() {
            this.page += 1;
            this.fetchPage();
        },
        previousPage() {
            if(page > 1) {
                this.page -= 1;
                this.fetchPage();
            }
        },
        fetchPage() {
            return fetch(`/api/users?page=${this.page}`) // Envía el número de página como parámetro de consulta a tu servidor node.js
                .then((res) => {
                    this.users = res.data;
                })
        }
    },
    mounted() {
        this.fetchPage();
    },
})
</script>

Angular

Quería incluir unos cuantos recortes para cada frontend framework, pero no uso Angular muy a menudo, lo siento.

Consejos y trucos: paginación con node.js y Mongoose

Enviar recuento del total de documentos

Siempre que sea posible, trate de sumar el número total de páginas o el número total de documentos.

De esa manera, el equipo de frontend puede construir algunos botones de paginación impresionantes.

Ejemplos de paginación Fuente: Cuál es tu paginación favorita? por Dawson Whitfield

Siempre aplique 'salto' primero, 'límite' después.

Un error común es utilizar el límite antes del salto

class UserPaginationExample {
    getAll(limit = 0, skip = 0) {
        return UsersModel.aggregate([
            { $match: {} },    // Esta es tu consulta
            { $limit: limit }, // Este es tu ´tamaño de la página´
            { $skip: skip },   // Siempre aplica "salto" antes de "límite".'
        ])  
    }
}

Este problema lo detectarás cuando vea que tu paginación está aplicando un límite de, por ejemplo, 30 documentos y 10 de salto, por lo que sólo obtiene 20 resultados.

Usar un filtro de clasificación para obtener mejores resultados de paginación

Por defecto, Mongo clasifica los documentos desde los más antiguos a los más nuevos.

Así, su primera página tendrá primero los registros más antiguos.

Cambia este comportamiento pasando un parámetro de ordenación en la colección de Mongo


class UserPaginationExample {
    getAll(limit = 0, skip = 0) {
        return UsersModel.find({})          // Es posible que desees agregar una consulta
                        .sort({ _id: -1 })  // Usa esto para clasificar los documentos por los más recientes primero
                        .skip(skip)         // Siempre aplica 'salto' antes de 'límite'
                        .limit(limit)       // Este es tu tamaño de la página
    }
}

Así es como se hace con el framework de aggregation de MongoDB

class UserPaginationExample {
    getAll(limit = 0, skip = 0) {
        return UsersModel.aggregate([
            { $match: {} },         // Esta es tu consulta
            { $sort: { _id: -1 } }  // Usa esto para clasificar los documentos por los más recientes primero
            { $skip: skip },        // Siempre aplica 'salto' antes de 'límite'
            { $limit: limit },      // Este es tu 'tamaño de página'
        ])  
    }
}

Conclusión

En realidad no importa si usas MongoDB con un driver nativo o Mongoose ODM, de hecho, no importa la base de datos que uses, el concepto es el mismo para todos.

La paginación es una solución muy potente y fácil de implementar cuando tienes muchos datos que pueden ser enviados al cliente.

Úsalo para mejorar el rendimiento, cargando y mostrando sólo lo necesario para tus usuarios.

Enviar más de 50 objetos nunca es una buena idea, en términos de experiencia de usuario y uso de datos.

Impleméntelo hoy para ahorrar ancho de banda (y dinero!), redujimos el 35% del uso de ancho de banda en nuestro último proyecto, ahorró un par de cientos de dólares de la factura de Netlify, nuestro cliente estaba muy contento.

Get the latest articles in your inbox.

Join the other 2000+ savvy node.js developers who get article updates. You will receive only high-quality articles about Node.js, Cloud Computing and Javascript front-end frameworks.


santypk4

CEO at Softwareontheroad.com