Obtención de datos en React de forma funcional con tecnología de TypeScript, io-ts y fp-ts


En los últimos días, he estado trabajando en una aplicación React. Es una aplicación sencilla que ni siquiera requiere una base de datos. Sin embargo, no quería incrustar todo el contenido en el JSX de la aplicación porque parte del mismo se actualizará con frecuencia. Así que decidí usar algunos archivos JSON simples para almacenar el contenido.

La aplicación es el sitio internet de una conferencia, y quería crear una página que se vea de la siguiente manera:

Para generar una página como la de la imagen anterior he guardado los datos en el siguiente archivo JSON:

(
    { "startTime": "08:00", "title": "Registration & Breakfast", "minuteCount": 60 },
    { "startTime": "09:00", "title": "Keynote", "minuteCount": 25 },
    { "startTime": "09:30", "title": "Discuss 1 (TBA)", "minuteCount": 25 },
    { "startTime": "10:00", "title": "Discuss 2 (TBA)", "minuteCount": 25 },
    { "startTime": "10:30", "title": "Discuss 3 (TBA)", "minuteCount": 25 },
    { "startTime": "10:55", "title": "Espresso Break", "minuteCount": 15 },
    { "startTime": "11:10", "title": "Discuss 4 (TBA)", "minuteCount": 25 },
    { "startTime": "11:40", "title": "Discuss 5 (TBA)", "minuteCount": 25 },
    { "startTime": "12:10", "title": "Discuss 6 (TBA)", "minuteCount": 25 },
    { "startTime": "12:35", "title": "Lunch, Networking & Group Pic", "minuteCount": 80 },
    { "startTime": "14:00", "title": "Discuss 7 (TBA)", "minuteCount": 25 },
    { "startTime": "14:30", "title": "Discuss 8 (TBA)", "minuteCount": 25 },
    { "startTime": "15:00", "title": "Discuss 9 (TBA)", "minuteCount": 25 },
    { "startTime": "15:25", "title": "Espresso Break", "minuteCount": 15 },
    { "startTime": "15:40", "title": "Discuss 10 (TBA)", "minuteCount": 25 },
    { "startTime": "16:10", "title": "Discuss 11 (TBA)", "minuteCount": 25 },
    { "startTime": "16:40", "title": "Discuss 12 (TBA)", "minuteCount": 25 },
    { "startTime": "17:10", "title": "Closing Remarks", "minuteCount": 25 }
)

El problema #

Si bien el uso de archivos JSON me facilita la vida, la obtención de datos en React es una tarea muy repetitiva y tediosa. Si eso no fuera lo suficientemente malo, los datos contenidos en una respuesta HTTP podrían ser completamente diferentes de lo que esperamos.

La naturaleza insegura de tipo de las llamadas de búsqueda es particularmente peligrosa para los usuarios de TypeScript porque compromete muchos de los beneficios de TypeScript. Así que decidí experimentar un poco para tratar de encontrar una buena solución automatizada.

He estado aprendiendo mucho sobre programación funcional y teoría de categorías en los últimos meses porque he estado escribiendo un libro titulado Programación funcional práctica con TypeScript.

No voy a profundizar demasiado en la teoría de categorías en esta publicación de weblog. Sin embargo, necesito explicar los conceptos básicos. La teoría de categorías outline algunos tipos que son particularmente útiles cuando se trata de efectos secundarios.

Los tipos de Teoría de categorías nos permiten expresar problemas potenciales usando el sistema de tipos y son beneficiosos porque obligan a nuestro código a manejar correctamente los efectos secundarios en el momento de la compilación. por ejemplo, el Both kind se puede usar para expresar que un tipo puede ser un tipo Left u otro tipo Proper. Él Both kind puede ser útil cuando queremos expresar que algo puede salir mal. por ejemplo, un fetch name puede devolver un error (izquierda) o algunos datos (derecha).

A) Asegurarse de que se manejen los errores. #

Quería asegurarme de que la devolución de mi fetch las llamadas son un Both instancia para asegurarnos de que no intentemos acceder a los datos sin antes garantizar que la respuesta no sea un error.

Tengo suerte porque no tengo que implementar el Both escribe. En su lugar, simplemente puedo usar la implementación embody en (fp-ts) (https://github.com/gcanti/fp-ts) módulo de código abierto. Él Both El tipo se outline mediante fp-ts de la siguiente manera:

declare kind Both<L, A> = Left<L, A> | Proper<L, A>;

B) Asegurarse de que los datos estén validados #

El segundo problema que quería resolver es que incluso cuando la solicitud devuelve algunos datos, su formato podría no ser el que espera la aplicación. Necesitaba algún mecanismo de validación en tiempo de ejecución para validar el esquema de la respuesta. Tengo suerte una vez más porque en lugar de implementar un mecanismo de validación de tiempo de ejecución desde cero, puedo usar otra biblioteca de código abierto: (io-ts)(https://github.com/gcanti/io-ts).

La solución #

TL;DR Esta sección explica los detalles de implementación de la solución. Siéntase libre de omitir esta parte y saltar a la sección “El resultado” si solo está interesado en la API del consumidor last.

El módulo io-ts nos permite declarar un esquema que se puede usar para realizar la validación en tiempo de ejecución. También podemos usar io-ts para generar tipos a partir de un esquema dado. Ambas características se muestran en el siguiente fragmento de código:

import * as io from "io-ts";

export const ActivityValidator = io.kind({
    startTime: io.string,
    title: io.string,
    minuteCount: io.quantity
});

export const ActivityArrayValidator = io.array(ActivityValidator);

export kind IActivity = io.TypeOf<typeof ActivityValidator>;
export kind IActivityArray = io.TypeOf<typeof ActivityArrayValidator>;

Podemos usar el decode método para validar que algunos datos se adhieren a un esquema. El resultado de la validación devuelto por decode es un Both instancia, lo que significa que obtendremos un error de validación (izquierda) o algunos datos válidos (derecha).

Mi primer paso fue envolver el fetch API, por lo que utiliza tanto fp-ts como io-ts para garantizar que la respuesta sea y Both que representa un error (izquierda) o algún dato válido (derecha). Al hacer esto, la promesa devuelta porfetch nunca es rechazado. En cambio, siempre se resuelve como un Both instancia:

import { Both, Left, Proper } from "fp-ts/lib/Both";
import { Kind, Errors} from "io-ts";
import { reporter } from "io-ts-reporters";

export async perform fetchJson<T, O, I>(
    url: string,
    validator: Kind<T, O, I>,
    init?: RequestInit
): Promise<Both<Error, T>> {
    strive {
        const response = await fetch(url, init);
        const json: I = await response.json();
        const consequence = validator.decode(json);
        return consequence.fold<Both<Error, T>>(
            (errors: Errors) => {
                const messages = reporter(consequence);
                return new Left<Error, T>(new Error(messages.be part of("n")));
            },
            (worth: T) => {
                return new Proper<Error, T>(worth);
            }
        );
    } catch (err) {
        return Promise.resolve(new Left<Error, T>(err));
    }
}

Luego creé un componente React llamado Distant eso toma un Both instancia como una de sus propiedades junto con algunas funciones de representación. Los datos pueden ser o null | Error o algún valor de tipo T.

Él loading La función se invoca cuando los datos son nullla error se invoca cuando los datos son un Error y el success la función se invoca cuando los datos son un valor de tipo T:

import React from "react";
import { Both } from "fp-ts/lib/both";

interface RemoteProps<T>  null, T>;
  loading: () => JSX.Factor,
  error: (error: Error) => JSX.Factor,
  success: (information: T) => JSX.Factor


interface RemoteState {}

export class Distant<T> extends React.Element<RemoteProps<T>, RemoteState> {

  public render() {
    return (
      <React.Fragment>
      {
        this.props.information.bimap(
          l => {
            if (l === null) {
              return this.props.loading();
            } else {
              return this.props.error(l);
            }
          },
          r => {
            return this.props.success(r);
          }
        ).worth
      }
      </React.Fragment>
    );
  }

}

export default Distant;

El componente anterior se utiliza para representar un Both instancia, pero no realiza ninguna operación de obtención de datos. En cambio, implementé un segundo componente llamado Fetchable que toma un url y un validator junto con algunos opcionales RequestInit configuración y algunas funciones de renderizado. El componente utiliza el fetch envoltorio y el validator para obtener algunos datos y validarlos. Luego pasa la resultante Both instancia a la Distant componente:

import { Kind } from "io-ts";
import React from "react";
import { Both, Left } from "fp-ts/lib/Both";
import { fetchJson } from "./consumer";
import { Distant } from "./distant";

interface FetchableProps<T, O, I> {
    url: string;
    init?: RequestInit,
    validator: Kind<T, O, I>
    loading: () => JSX.Factor,
    error: (error: Error) => JSX.Factor,
    success: (information: T) => JSX.Factor
}

interface FetchableState<T>  null, T>;


export class Fetchable<T, O, I> extends React.Element<FetchableProps<T, O, I>, FetchableState<T>> {

    public constructor(props: FetchableProps<T, O, I>) {
        tremendous(props);
        this.state = {
            information: new Left<null, T>(null)
        }
    }

    public componentDidMount() {
        (async () => {
            const consequence = await fetchJson(
                this.props.url,
                this.props.validator,
                this.props.init
            );
            this.setState({
                information: consequence
            });
        })();
    }

    public render() {
        return (
            <Distant<T>
                loading={this.props.loading}
                error={this.props.error}
                information={this.state.information}
                success={this.props.success}
            />
        );
    }

}

El resultado #

He publicado todo el código fuente anterior como un módulo llamado recuperable por reacción. Puede instalar el módulo usando el siguiente comando:

npm set up io-ts fp-ts react-fetchable

A continuación, puede importar el Fetchable componente de la siguiente manera:

import { Fetchable } from "react-fetchable";

En este punto puedo implementar la página que describí al principio:

import React from "react";
import Container from "../../elements/container/container";
import Part from "../../elements/part/part";
import Desk from "../../elements/desk/desk";
import { IActivityArray, ActivityArrayValidator } from "../../lib/area/varieties";
import { Fetchable } from "react-fetchable";

interface ScheduleProps {}

interface ScheduleState {}

class Schedule extends React.Element<ScheduleProps, ScheduleState> {
  public render() {
    return (
      <Container>
        <Part title="Schedule">
          <p>
            Lorem ipsum dolor sit amet, consectetur adipiscing elit,
            sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
          </p>
          <Fetchable
            url="/information/schedule.json"
            validator={ActivityArrayValidator}
            loading={() => <div>Loading...</div>}
            error={(e: Error) => <div>Error: {e.message}</div>}
            success={(information: IActivityArray) => {
              return (
                <Desk
                  headers={("Time", "Exercise")}
                  rows={information.map(a => (`${a.startTime}`, a.title))}
                />
              );
            }}
          />
        </Part>
      </Container>
    );
  }
}

export default Schedule;

Puedo pasar la URL /information/schedule.json hacia Fetchable componente junto con un validador ActivityArrayValidator. El componente entonces:

  1. Prestar Loading...
  2. Obtener los datos
  3. Representar una tabla si los datos son válidos
  4. Procesar un error es que los datos no se pueden cargar no se adhieren al validador

Estoy contento con esta solución porque es de tipo seguro, declarativa y solo lleva unos segundos ponerla en funcionamiento. Espero que este submit te haya resultado interesante y que pruebes react-fetchable.

Además, si está interesado en la programación funcional o TypeScript, consulte mi próximo libro Programación funcional práctica con TypeScript.

44

Prestigio

44

Prestigio

Related Articles

BT Group pasa de los mainframes a la nube con Kyndryl

Duncan es un editor galardonado con más de 20...

Comments

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Same Category

spot_img

Stay in touch!

Follow our Instagram