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 }
]
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>;
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>;
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));
}
}
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:
- Render
Loading...
- Fetch the data
- Render a table if the data is valid
- 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)
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>'.
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 ofEither
anymore.It seems the correct implementation is now:
@remojansen do you agree with this implementation?
gracias!
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.
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: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 theEither<Errors, T>
intoEither<Error, T>
, you could usemapLeft
. I doubt it would ever matter in practice, since this is not a tight loop, butmapLeft
is more efficient since it doesn't construct a secondRight
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!