DEV Community

Remo H. Jansen
Remo H. Jansen

Posted on

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

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:

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 }
]
Enter fullscreen mode Exit fullscreen mode

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 included in the 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>;
Enter fullscreen mode Exit fullscreen mode

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.

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>;
Enter fullscreen mode Exit fullscreen mode

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 an 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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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}
            />
        );
    }

}

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You can then import the Fetchable component as follows:

import { Fetchable } from "react-fetchable";
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (6)

Collapse
 
austinrivas profile image
Austin Rivas

Could you explain your use of this.props.data.bimap?

I cannot find any reference to it in fp-ts docs and it throws the following compilation error.

TS2339: Property 'bimap' does not exist on type 'Either<Error | null, T>'.   Property 'bimap' does not exist on type 'Left<Error | null>'.

Collapse
 
austinrivas profile image
Austin Rivas • Edited

Ok, I can see that the fp-ts api was changed during the 2.0 update.

bimap is now its own function and not a property of Either anymore.

It seems the correct implementation is now:

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

  public render() {
    return (
      <React.Fragment>
        {
          pipe(this.props.data, fold(
          l => {
            if (l === null) {
              return this.props.loading();
            } else {
              return this.props.error(l);
            }
          },
          r => {
            return this.props.success(r);
          }))
        }
      </React.Fragment>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

@remojansen do you agree with this implementation?

Collapse
 
josuevalrob profile image
Josue Valenica

gracias!

Collapse
 
davidchase profile image
David Chase

Neat write up, have you thought about using Free or Tagless ? something along the lines of writing the program without committing to a specific type until you write the program's interpreter. Essentially you could have used any mechanism of fetching that could be a Future/Task etc and would compose with the other types you already provided from fp-ts.

Collapse
 
remojansen profile image
Remo H. Jansen

No, I didn't think about Free or Tagless but I will take a look. Thanks for the suggestion!

I've been thinking that better than Promise<Either<Error>, T> I could use TaskEither:

TaskEither<L, A> represents an asynchronous computation that either yields a value of type A or fails yielding an
error of type L.

Collapse
 
austindd profile image
Austin

First of all I really like this write-up. It demonstrates a very useful functional programming pattern! But did notice that instead of using fold to transform the Either<Errors, T> into Either<Error, T>, you could use mapLeft. I doubt it would ever matter in practice, since this is not a tight loop, but mapLeft is more efficient since it doesn't construct a second Right value every time it succeeds. Other than that (and the fact that the code is now out of date with the library API), this is super good!