12

Data fetching in React the functional way powered by TypeScript, io-ts & fp-...

 3 years ago
source link: http://blog.wolksoftware.com/data-fetching-in-react-the-functional-way-powered-by-typescript-io-ts-fp-ts
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
January 20, 2019

Data fetching in React the functional way powered by TypeScript, io-ts & fp-ts

wi21bxgi8149ukwcsngu.png

Over the past few days, I’ve been working on a React application. It is a straightforward application that doesn’t even require a database. However, I didn’t want to embed all the content into the application’s JSX because some of it will be updated frequently. So I decided to use a few simple JSON files to store the contents.

The application is the website for a conference, and I wanted to build a page that looks as follows:

g84x1rm643k0pbt4juxd.png

To generate a page like the one in the previous image I have stored the data in the following JSON file:

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

The problem #

While using JSON files makes my life easier, data fetching in React is a very repetitive and tedious task. If that wasn’t bad enough, the data contained in an HTTP response could be completely different from what we are expecting.

The type-unsafe nature of fetch calls is particularly dangerous for TypeScript users because it compromises many of the benefits of TypeScript. So I decided to experiment a little bit to try to come up with a nice automated solution.

I have been learning a lot about functional programming and Category Theory over the past few months because I’ve been writing a book titled Hands-On Functional Programming with TypeScript.

I’m not going to get too much into Category Theory in this blog post. However, I need to explain the basics. Category Theory defines some types that are particularly useful when dealing with side effects.

The Category Theory types allow us to express potential problems using the type system and are beneficial because they force our code to handle side effects correctly at compilation time. For example, the Either type can be used to express that a type can be either a type Left or another type Right. The Either type can be useful when we want to express that something can go wrong. For example, a fetch call can return either an error (left) or some data (right).

A) Ensure that errors are handled #

I wanted to make sure that the return of my fetch calls are an Either instance to ensure that we don’t try to access the data without first guaranteeing that the response is not an error.

I’m lucky because I don’t have to implement the Either type. Instead I can simply use the implementation include in the [fp-ts](https://github.com/gcanti/fp-ts) open source module. The Either type is defined by fp-ts as follows:

declare type Either<L, A> = Left<L, A> | Right<L, A>;

B) Ensure that data is validated #

The second problem that I wanted to solve is that even when the request returns some data, its format could be not what the application is expecting. I needed some runtime validation mechanism to validate the schema of the response. I’m lucky once more because instead of implementing a runtime validation mechanism from scratch, I can use another open source library: [io-ts](https://github.com/gcanti/io-ts).

The solution #

TL;DR This section explains the implementation details of the solution. Feel free to skip this part and jump into “The result” section if you are only interested in the final consumer API.

The io-ts module allows us to declare a schema that can be used to perform validation at runtime. We can also use io-ts to generate types from a given schema. Both of these features are showcased in the following code snippet:

import * as io from "io-ts";

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

export const ActivityArrayValidator = io.array(ActivityValidator);

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

We can use the decode method to validate that some data adheres to a schema. The validation result returned by decode is an Either instance, which means that we will either get a validation error (left) or some valid data (right).

My first step was to wrap the fetch API, so it uses both fp-ts and io-ts to ensure that the response is and Either that represents an error (left) or some valid data (right). By doing this, the promise returned byfetch is never rejected. Instead, it is always resolved as an Either instance:

import { Either, Left, Right } from "fp-ts/lib/Either";
import { Type, Errors} from "io-ts";
import { reporter } from "io-ts-reporters";

export async function fetchJson<T, O, I>(
    url: string,
    validator: Type<T, O, I>,
    init?: RequestInit
): Promise<Either<Error, T>> {
    try {
        const response = await fetch(url, init);
        const json: I = await response.json();
        const result = validator.decode(json);
        return result.fold<Either<Error, T>>(
            (errors: Errors) => {
                const messages = reporter(result);
                return new Left<Error, T>(new Error(messages.join("\n")));
            },
            (value: T) => {
                return new Right<Error, T>(value);
            }
        );
    } catch (err) {
        return Promise.resolve(new Left<Error, T>(err));
    }
}

Then I created a React component named Remote that takes an Either instance as one of its properties together with some rendering functions. The data can be either null | Error or some value of type T.

The loading function is invoked when the data is null, the error is invoked when the data is an Error and the success function is invoked when data is a value of type T:

import React from "react";
import { Either } from "fp-ts/lib/either";

interface RemoteProps<T> {
  data: Either<Error | null, T>;
  loading: () => JSX.Element,
  error: (error: Error) => JSX.Element,
  success: (data: T) => JSX.Element
}

interface RemoteState {}

export class Remote<T> extends React.Component<RemoteProps<T>, RemoteState> {

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

}

export default Remote;

The above component is used to render an Either instance, but it doesn’t perform any data fetching operations. Instead, I implemented a second component named Fetchable which takes an url and a validator together with some optional RequestInit configuration and some rendering functions. The component uses the fetch wrapper and the validator to fetch some data and validate it. It then passes the resulting Either instance to the Remote component:

import { Type } from "io-ts";
import React from "react";
import { Either, Left } from "fp-ts/lib/Either";
import { fetchJson } from "./client";
import { Remote } from "./remote";

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

interface FetchableState<T> {
    data: Either<Error | null, T>;
}

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

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

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

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

}

The result #

I have released all the preceding source code as a module named react-fetchable. You can install the module using the following command:

npm install io-ts fp-ts react-fetchable

You can then import the Fetchable component as follows:

import { Fetchable } from "react-fetchable";

At this point I can implement the page that I described at the beguinning:

import React from "react";
import Container from "../../components/container/container";
import Section from "../../components/section/section";
import Table from "../../components/table/table";
import { IActivityArray, ActivityArrayValidator } from "../../lib/domain/types";
import { Fetchable } from "react-fetchable";

interface ScheduleProps {}

interface ScheduleState {}

class Schedule extends React.Component<ScheduleProps, ScheduleState> {
  public render() {
    return (
      <Container>
        <Section 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="/data/schedule.json"
            validator={ActivityArrayValidator}
            loading={() => <div>Loading...</div>}
            error={(e: Error) => <div>Error: {e.message}</div>}
            success={(data: IActivityArray) => {
              return (
                <Table
                  headers={["Time", "Activity"]}
                  rows={data.map(a => [`${a.startTime}`, a.title])}
                />
              );
            }}
          />
        </Section>
      </Container>
    );
  }
}

export default Schedule;

I can pass the URL /data/schedule.json to the Fetchable component together with a validator ActivityArrayValidator. The component will then:

  1. Render Loading...
  2. Fetch the data
  3. Render a table if the data is valid
  4. Render an error is the data cannot be loaded doesn’t adhere to the validator

I’m happy with this solution because it is type-safe, declarative and it only takes a few seconds to get it up and running. I hope you have found this post interesting and that you try react-fetchable.

Also, if you are interested in Functional Programming or TypeScript, please check out my upcoming book Hands-On Functional Programming with TypeScript.

n5sfa1euspqvdye5cky6.png
Kudos

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK