Fetching Data in ReasonML Pt. 1

Understanding the basics

A. Sharif
4 min readMay 4, 2018

Introduction

Most applications we build, need to interact with a server at some point. Fetching data from and sending data back to an endpoint is a common task we encounter when writing an app.
In this short tutorial we will learn how this can be accomplished in ReasonML.

What do we need to be enable to interact with an API endpoint?

bs-fetch is a library, which forms a wrapper around the fetch API.

bs-json is a compositional JSON encode/decode library for BuckleScript.

These two libraries will build the basis for interacting with an external API endpoint, everything else is available via ReasonML itself.

The outcome for this tutorial should be:

  1. Understand fetching basics
  2. Understand how to encode/decode data

Fetching Data Example

For the following tutorial we will use an API endpoint served via http://localhost:3000/users. Our initial users structure is as follows:

{
"users": [
{
"id": 1,
"name": "User A"
},
{
"id": 2,
"name": "User B"
},
{
"id": 3,
"name": "User C"
}
]
}

What we can derive from this information, is that our user type can be defined as:

type user = {
id: int,
name: string,
};

Based on the above structure, we can also already define the possible states we want to display.

type state =
| NotAsked
| Loading
| Failure
| Success(list(user));

We have four possible states that we want represented inside our UI. If the loading has never been triggered, we would like to display a load button, else we are either currently loading the data or have been successful / encountered an error. This state shape prevents us from ever falsely displaying a loading icon while also displaying the user list f.e.

Now that we have our state shape defined, the next step is to figure out how to decode the data we want to retrieve. bs-json exposes Json.Encode/Json.Decode modules and we will use the Decode module to convert our expected users list back to Reason land.

module Decode = {
let user = user =>
Json.Decode.{
id: field("id", int, user),
name: field("name", string, user),
};
};

By defining the expected data structure and mapping via field, we can decode id and respectively map back to our user record. For example by defining:

field("id", int, user)

we ensure that id is an integer. What’s missing is the possibility to map over all the users and decode them one by one. Let’s extend our Decode module.

module Decode = {
// ...
let users = json : list(user) => Json.Decode.list(user, json);
};

This will enable us to use Decode.users later, when fetching the data. What’s still missing is the actual fetching operation, if you recall we already defined a url.

let url = "http://localhost:3000/users";

To be able to make a request against the defined url, we will use bs-fetch, which is a thin wrapper around the Fetch API and wrap the operation inside a promise.

let fetchUsers = () =>
Js.Promise.(
Fetch.fetch(url)
|> then_(Fetch.Response.json)
|> then_(json =>
json |> Decode.users |> (users => Some(users) |> resolve)
)
|> catch(_err => resolve(None))
);

We also made our fetchUsers function lazy, meaning we will trigger the operation when needed. The approach is to try to run a fetch operation, decode the result and catch any possible errors. We will return an option, so we can react to the result accordingly. The option type is exposed by the standard library and is a variant with following definition:

type option(‘a) = None | Some(‘a);

Our next step is implement a component and tie everything together. Our reducerComponent will include the following actions:

type action =
| LoadUsers
| LoadedUsers(list(user))
| LoadUsersFailed;
let component = ReasonReact.reducerComponent("FetchComponent");

Our initial state is that no fetch has been run, so we will default toNotAsked

let make = _children => {
...component,
initialState: () => NotAsked,
};

The reducer function needs to handle all possible actions, that we previously defined.

reducer: (action, _state) =>
switch (action) {
| LoadUsers =>
ReasonReact.UpdateWithSideEffects(
Loading,
(
self =>
Js.Promise.(
fetchUsers()
|> then_(result =>
switch (result) {
| Some(users) =>
resolve(self.send(LoadedUsers(users)))
| None => resolve(self.send(LoadUsersFailed))
}
)
|> ignore
)
),
)
| LoadedUsers(users) => ReasonReact.Update(Success(users))
| LoadUsersFailed => ReasonReact.Update(Failure)
},

While LoadedUsers and LoadUsersFailed, depending on the outcome of the fetch operation, either update the state with Success or Failure, LoadUsers will need more explaining.

| LoadUsers =>
ReasonReact.UpdateWithSideEffects(
Loading,
(
self =>
/* ... */
),
)

With the LoadUsers action we need to do two things, first update the current state to Loading and second call our previously defined fetchUsers function start fetching the actually needed data. ReasonReact.UpdateWithSideEffects enables us to update the state and define a unsafe operation as a second argument.

self =>
Js.Promise.(
fetchUsers()
|> then_(result =>
switch (result) {
| Some(users) => resolve(self.send(LoadedUsers(users)))
| None => resolve(self.send(LoadUsersFailed))
}
)
|> ignore
)

If you recall, we defined our fetchUsers to return an option type. This enables us to pattern match over the result and either update the state with the newly defined users or declare the fetch as failed.

Finally inside render we handle all possible states.

render: self =>
switch (self.state) {
| NotAsked =>
<div>
(str("Click to start load Users"))
<button onClick=(_event => self.send(LoadUsers))>
(str("Load Users"))
</button>
</div>
| Loading => <div> (str("Loading...")) </div>
| Failure => <div> (str("Something went wrong!")) </div>
| Success(users) =>
<div>
<h2> (str("Users")) </h2>
<ul>
(
users
|> List.map(user =>
<li key=(string_of_int(user.id))>
(str(user.name))
</li>
)
|> Array.of_list
|> ReasonReact.array
)
</ul>
</div>
},

In this specific case, we don’t load automatically inside didMount, but rather initially display a load button. Once the load action has been triggered we then either display a loading text or the final result, which could be a list of uses or an error message.

Find the complete example here.

Summary

We should have a basic understanding of how to fetch data via ReasonML now. In part 2 we will try to understand how we to create or update and delete data.

Links

bs-fetch

bs-json

Basic Example

If there are better ways to solve these problems or if you have any questions, please leave a comment here or on Twitter.

--

--

A. Sharif

Focusing on quality. Software Development. Product Management.