Scroll infinito con React.js y Go
Tags: react, go, javascript, programming
Published: 2021-03-21 22:17:32


> *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](https://media.giphy.com/media/JdeMMnTNJzEWSQrEjr/giphy.gif)

# 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](https://golang.org/).

Como requisitos debemos de contar con la [instalación](https://golang.org/doc/install) y configuración del lenguaje. Para asegurarnos de que contamos con **go** en nuestro sistema ejecutamos:
```sh
$ go version
```
como resultado debemos de tener similar, dependiendo del sistema operativo que se utilice:
```sh
$ 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](https://apiumhub.com/es/tech-blog-barcelona/arquitectura-hexagonal/) teniendo como resultado la siguiente estructura de directorios:
![go-infinite-scroll](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9xdcdbg1j4nq0rxlzjfo.png)

Separaremos la lógica de nuestro servidor y la configuración de rutas para poder incluir mas adelante nuevos endpoint a nuestro servicio.
```go
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)
}
```
```go
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.
```go
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.

```go
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).

```go
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:
```sh
$ go run api/main.go
```
y podremos ver nuestra app corriendo en el puerto `:8080`
```sh
[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](https://nodejs.org/es/)), dentro de la carpeta **src** de nuestro proyecto crearemos la siguiente estructura de carpetas.
![tree-scroll-app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ilx8s2iwol47o59fqxbd.png)

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

`src/app/hooks/useScroll.js`

```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`
```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:
```css
.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 ;)*

Puedes ver mas publicaciones visitando mi sitio web [literalcoder.com](https://literalcoder.com) :)


#### Repositorio de GitHub (Frontend): {% github Josh2604/react-infinite-scroll %}

#### Repositorio de GitHub (Backend): {% github https://github.com/Josh2604/go-infinite-scroll %}