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 null
la 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:
- Prestar
Loading...
- Obtener los datos
- Representar una tabla si los datos son válidos
- 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