DEV Community

loading...
Cover image for Scroll infinito con React.js y Go

Scroll infinito con React.js y Go

Josué Andrés
Ingeniero de Software y apasionado de la programación.
・6 min read

Bienvenidos nuevos lectores, espero que les sea de utilidad el contenido del siguiente post...

React se ha convertido en una de las librerías mas populares en cuanto a creación de interfaces de usuario se refiere, y en esta ocasión haremos uso de esta para la creación de un hook que nos permitirá gestionar la funcionalidad de un infinite scroll.
inifinte-scroll

Backend

Iniciaremos creando el API para nuestra implementación la cual de desarrollaremos en uno de los lenguajes que hasta el momento ha ido ganando mucha popularidad entre la comunidad de desarrolladores(incluyéndome), sí, me refiero a go.

Como requisitos debemos de contar con la instalación y configuración del lenguaje. Para asegurarnos de que contamos con go en nuestro sistema ejecutamos:

$ go version
Enter fullscreen mode Exit fullscreen mode

como resultado debemos de tener similar, dependiendo del sistema operativo que se utilice:

$ go version go1.16 darwin/amd64
Enter fullscreen mode Exit fullscreen mode

Una vez que contamos con go en nuestro sistema comenzaremos con la creación de la estructura del proyecto, haremos uso de las denominadas arquitecturas limpias como la hexagonal teniendo como resultado la siguiente estructura de directorios:
go-infinite-scroll

Separaremos la lógica de nuestro servidor y la configuración de rutas para poder incluir mas adelante nuevos endpoint a nuestro servicio.

package server

import (
    "net/http"

    "github.com/Josh2604/go-infinite-scroll/api/dependencies"
    "github.com/gin-gonic/gin"
)

func routes(router *gin.Engine, handlers *dependencies.Handlers) {
    postRoutes(router, handlers)
}

func postRoutes(router *gin.Engine, handlers *dependencies.Handlers) {
    router.GET("/", func(c *gin.Context) {
        c.JSON(http.StatusOK, "Running")
    })
    router.POST("/posts", handlers.GetPosts.Handle)
}
Enter fullscreen mode Exit fullscreen mode
package server

import (
    "github.com/Josh2604/go-infinite-scroll/api/dependencies"
    "github.com/gin-contrib/cors"
    "github.com/gin-gonic/gin"
)

const port = ":8080"

func Start() {
    router := gin.New()

    handlers := dependencies.Exec()

    router.Use(cors.Default())
    routes(router, handlers)

    if err := router.Run(port); err != nil {
        panic(err)
    }
}

Enter fullscreen mode Exit fullscreen mode

Definiremos nuestra carpeta de dependencias y realizaremos su inyección al momento del arranque de nuestro servicio.

package dependencies

import (
    "github.com/Josh2604/go-infinite-scroll/api/entrypoints"
    "github.com/Josh2604/go-infinite-scroll/api/usecases/getfeeds"
)

type Handlers struct {
    GetPosts entrypoints.Handler
}

func Exec() *Handlers {
    // UseCases
    postsUseCases := &getfeeds.Implementation{}

    // Handlers
    handlers := Handlers{}

    handlers.GetPosts = &entrypoints.GetPosts{
        GetPostsUseCase: postsUseCases,
    }

    return &handlers
}

Enter fullscreen mode Exit fullscreen mode

Definimos los puntos de entrada de nuestra app dentro de la carpeta entrypoints estos se encargaran de ser los handlers de nuestras rutas.

package entrypoints

import (
    "net/http"

    "github.com/Josh2604/go-infinite-scroll/api/core/contracts/getposts"
    apiErrors "github.com/Josh2604/go-infinite-scroll/api/errors"
    "github.com/Josh2604/go-infinite-scroll/api/usecases/getfeeds"
    "github.com/gin-gonic/gin"
)

type GetPosts struct {
    GetPostsUseCase getfeeds.UseCase
}

func (useCase *GetPosts) Handle(c *gin.Context) {
    err := useCase.handle(c)
    if err != nil {
        c.JSON(err.Status, err)
    }
}

func (useCase *GetPosts) handle(c *gin.Context) *apiErrors.Error {
    var request getposts.Paginator
    errq := c.BindJSON(&request)
    if errq != nil {
        return apiErrors.NewBadRequest("Invalid Request Parameters", errq.Error())
    }

    response, err := useCase.GetPostsUseCase.GetPosts(c, &request)
    if err != nil {
        c.JSON(http.StatusInternalServerError, "Error!")
        return nil
    }

    c.JSON(http.StatusOK, &response)
    return nil
}
Enter fullscreen mode Exit fullscreen mode

Por último crearemos el caso de uso para nuestro servicio de scroll infinito, no utilizaremos una base de datos, se hará uso de un archivo json estático que contiene una lista de 100 posts de prueba. Se puede hacer la implementación de una base de datos más adelante debido a la separación de las capas del servicio (Las bondades que nos permite el uso de arquitecturas limpias).

package getfeeds

import (
    "context"
    "encoding/json"
    "io/ioutil"
    "math"
    "os"

    "github.com/Josh2604/go-infinite-scroll/api/core/contracts/getposts"
    "github.com/Josh2604/go-infinite-scroll/api/core/entities"
)

type UseCase interface {
    GetPosts(ctx context.Context, paginator *getposts.Paginator) (*getposts.Response, error)
}

type Implementation struct {
}

// GetFeeds -
func (useCase *Implementation) GetPosts(ctx context.Context, paginator *getposts.Paginator) (*getposts.Response, error) {
    var pageNumber, items = paginator.PageNo, paginator.Limit
    posts := getPosts()
    total := len(posts)

    start := (pageNumber - 1) * items
    end := pageNumber * items

    div := float64(total) / float64(items)
    totalPages := math.Trunc(div)

    HASMORE := true

    if (pageNumber + 1) > int(totalPages) {
        HASMORE = false
    }

    if (paginator.PageNo * paginator.Limit) > total {
        start = 0
        end = 0
    }

    response := getposts.Response{
        Total:       total,
        CurrentPage: pageNumber,
        PagesNo:     int(totalPages),
        HasMore:     HASMORE,
        Items:       posts[start:end],
    }
    return &response, nil
}

func getPosts() []entities.Post {
    posts := make([]entities.Post, 100)

    raw, err := ioutil.ReadFile("feeds.json")
    if err != nil {
        os.Exit(1)
    }
    errJ := json.Unmarshal(raw, &posts)
    if errJ != nil {
        os.Exit(1)
    }
    return posts
}
Enter fullscreen mode Exit fullscreen mode

Ejecutamos el comando:

$ go run api/main.go
Enter fullscreen mode Exit fullscreen mode

y podremos ver nuestra app corriendo en el puerto :8080

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /                         --> github.com/Josh2604/go-infinite-scroll/api/server.postRoutes.func1 (2 handlers)
[GIN-debug] POST   /posts                    --> github.com/Josh2604/go-infinite-scroll/api/entrypoints.Handler.Handle-fm (2 handlers)
[GIN-debug] Listening and serving HTTP on :8080
Enter fullscreen mode Exit fullscreen mode

El principal beneficio que nos provee el uso de arquitecturas limpias es el desacople de las capas de nuestra aplicación y mediante la inyección de dependencias poder agregar o quitar funcionalidades a la misma, logrando que los cambios afecten lo menos posible a la estructura de nuestra proyecto.

Frontend

Para comenzar con el frontend crearemos un nuevo proyecto ejecutando npx create-react-app react-infinite-scroll (tener instalado node.js), dentro de la carpeta src de nuestro proyecto crearemos la siguiente estructura de carpetas.
tree-scroll-app

Lo primero que haremos es crear un hook donde encapsularemos la funcionalidad de nuestra API.

src/app/hooks/useScroll.js

import axios from 'axios';
import { useCallback, useEffect, useState } from 'react';

export default function useScroll({ pageNo, limit, apiPath }) {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);
  const [data, setData] = useState([]);
  const [hasMore, setHasMore] = useState(true);
  const [details, setDetails] = useState({
    "total": 0,
    "pages": 0
  });

  const GetData = useCallback(async () => {
    try {
      let cancel;
      let config = {
        method: 'POST',
        url: apiPath,
        data: {
          page_no: pageNo,
          limit: limit ? limit : 10
        },
        cancelToken: new axios.CancelToken(c => cancel = c)
      }

      const response = await axios(config);

      const data = response.data;

      setData(prevData => {
        return [...new Set(prevData), ...data.items]
      });
      setDetails({
        total: data.total,
        pages: data.pages_no
      });
      setHasMore(data.has_more);

      setLoading(false);
      return () => cancel();
    } catch (error) {
      setError(true);
      setLoading(false);
      if (axios.isCancel(error)) {
        return
      }
    }
  }, [pageNo, apiPath, limit]);

  useEffect(() => {
    GetData();
  }, [GetData]);

  return { loading, error, data, hasMore, details };
};
Enter fullscreen mode Exit fullscreen mode

Lo siguiente es crear el componente de react e importar nuestro hook creado anteriormente, la funcion HandlerScroll de nuestro componente la utilizaremos para calcular el ancho de nuestro contenedor una vez que sobrepasemos el ancho al hacer scroll sobre el contenedor incrementaremos en uno el valor actual de la variable pageNumber esto provocara que nuestro hook se ejecute y nos devuelva los nuevos resultados.

src/app/components/InfineScroll/index.js

import React, { useState } from 'react';
import useScroll from './../../hooks/useScroll';
import './styles.css';

function ScrollImplementation() {
  const [pageNumber, setPageNumber] = useState(1)
  const { loading, error, data, hasMore, details } = useScroll({ pageNo: pageNumber, limit: 10, apiPath: 'http://service-name/posts' });

  function HandlerScroll(evt) {
    const { scrollTop, clientHeight, scrollHeight } = evt.currentTarget;
    if (scrollHeight - scrollTop === clientHeight && loading === false && hasMore === true) {
      setPageNumber(prevPageNumber => prevPageNumber + 1);
    }
  }
  return (
    <div className="container">
      <h1 className="display-6">Posts</h1>
      <span class="badge rounded-pill bg-primary"> No. paginas: {details.pages}</span>
      <span class="badge rounded-pill bg-info text-dark">Items: {details.total}</span>
      <div className="container-fluid posts-container"
        onScroll={HandlerScroll}
      >
        {
          data.map((element, key) => {
            return (
              <div key={key} className="card card-container">
                <div className="card-body">
                  <h5 className="card-title">{element.title}</h5>
                  <p className="card-text">{element.body}</p>
                </div>
              </div>
            )
          })
        }
        <div>{error && 'Error...'}</div>
      </div>
    </div>
  )
}
export default ScrollImplementation;
Enter fullscreen mode Exit fullscreen mode

Por último agregaremos algunos estilos a nuestro componente:

.posts-container{
  max-height: 44em;
  overflow-y: scroll;
  overflow-x: hidden;
}
.card-container{
  margin: 1em 0em 1em 0em;
}
Enter fullscreen mode Exit fullscreen mode

Si te fue de ayuda, comparte este contenido con quien creas que le sera util ;)

Repositorio de GitHub (Frontend):

GitHub logo Josh2604 / react-infinite-scroll

Implementación de scroll infinito en Recact.js

Repositorio de GitHub (Backend):

GitHub logo Josh2604 / go-infinite-scroll

Go infinite scroll backend.

Posts API

Frontend

Implementación con React

Features

  • Implementación de scroll infinito

GET /

POST /posts

Paginación de posts

request:

{
    "page_no": 1
    "limit":10
}
Enter fullscreen mode Exit fullscreen mode

response:

{
    "total": 100
    "current_page": 2
    "pages_no": 20
    "has_more": true,
    "items": [
        {
            "user_id": 0,
            "id": 6,
            "title": "dolorem eum magni eos aperiam quia",
            "body": "ut aspernatur corporis harum nihil quis provident sequi\nmollitia nobis aliquid molestiae\nperspiciatis et ea nemo ab reprehenderit accusantium quas\nvoluptate dolores velit et doloremque molestiae"
        },
        {
            "user_id": 0,
            "id": 7,
            "title": "magnam facilis autem",
            "body
Enter fullscreen mode Exit fullscreen mode

Discussion (0)