Publicado
- 6 min read
Scroll infinito con React.js y Go
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.
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
como resultado debemos de tener similar, dependiendo del sistema operativo que se utilice:
$ go version go1.16 darwin/amd64
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:
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)
}
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)
}
}
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
}
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
}
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
}
Ejecutamos el comando:
$ go run api/main.go
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
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.
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 }
}
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
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;
}
Si te fue de ayuda, comparte este contenido con quien creas que le sera util ;)