ReasonML for production React Apps? 🤔 (Part 2)
Tags: reason, react, javascript
Published: 2019-05-03 13:41:12

---
title: ReasonML for production React Apps? 🤔 (Part 2)
series: ReasonML for production React Apps? 🤔
published: true
description: 
tags: ReasonML, React, JavaScript
canonical_url: https://tinloof.com/blog/reasonml-for-production-react-apps-part-2/
---

I like to go to the Roadmap of ReasonML from time to time to get excited about what's getting cooked. Whenever I visit the page, I can't help but notice these 2 points:


<figure>
  <img src="https://thepracticaldev.s3.amazonaws.com/i/4w5vix6ye46qjbtv9ku1.png" alt="ReasonML Roadmap">
  <figcaption>ReasonML Roadmap</figcaption>
</figure>

So the official documentation itself admits that the async story and JSON handling are not that great. 

This is exactly why, in this second part of the series, we'll build a simple Login page for our words counter. By doing that, I'm pretty sure we will bump into these 2 points among other things and have a better overview of building React applications in Reason.

Just like the previous part, I will approach building this feature in iterations and report what I like/dislike.

<figure>
  <img src="https://thepracticaldev.s3.amazonaws.com/i/kqhk965ppigtor0dlppk.gif" alt="Final Resul">
  <figcaption>Final Result</figcaption>
</figure>

You can find the final code [here](https://github.com/siffogh/words-counter).

# Iteration #1: separate the words counter into its own component
Our application will no longer be just a words counter page. Therefore, we have to move the content of the App component into a separate component and create another component for the Login page.

```ocaml
/* src/Home.re */

[%bs.raw {|require('./Home.css')|}];

let countWordsInString = text => {
  let spacesRegex = Js.Re.fromString("\s+");

  switch (text) {
  | "" => 0
  | noneEmptyText =>
    noneEmptyText
    |> Js.String.trim
    |> Js.String.splitByRe(spacesRegex)
    |> Js.Array.length
  };
};

module Times = {
  [@bs.module "./icons/times.svg"] [@react.component]
  external make: (~height: string) => React.element = "default";
};

module Copy = {
  [@bs.module "./icons/copy.svg"] [@react.component]
  external make: (~height: string) => React.element = "default";
};

module CopyClipboard = {
  [@bs.module "react-copy-to-clipboard"] [@react.component]
  external make: (~text: string, ~children: React.element) => React.element =
    "CopyToClipboard";
};

[@react.component]
let make = () => {
  let (text, setText) = React.useState(() => "");

  let handleTextChange = e => ReactEvent.Form.target(e)##value |> setText;

  let handleClearClick = _ => setText(_ => "");

  let wordsCountText =
    (text |> countWordsInString |> string_of_int) ++ " words";

  <div className="Home">
    <div className="header">
      <h3> {"Words Counter" |> ReasonReact.string} </h3>
      <span> {ReasonReact.string(wordsCountText)} </span>
    </div>
    <textarea
      placeholder="Express yourself..."
      value=text
      onChange=handleTextChange
    />
    <div className="footer">
      <Button
        title="Clear text"
        onClick=handleClearClick
        disabled={String.length(text) === 0}>
        <Times height="20px" />
      </Button>
      <CopyClipboard text>
        <Button
          title="Copy text"
          disabled={String.length(text) === 0}
          category=Button.PRIMARY>
          <Copy height="20px" />
        </Button>
      </CopyClipboard>
    </div>
  </div>;
};

```

The App component is also refactored to just render a Home component:


```ocaml
/* src/App.re */

[@react.component]
let make = () => {
  <div className="App"> <Home /> </div>;
};
```

> So far, we've just made minor changes and there is nothing for me to like/dislike.

# Iteration 2: add a Router
To make it possible to navigate to our Home and Login pages, we need to add a Router. 

Let's first build a simple Login page that we can navigate to:

```ocaml
/* src/Login.re */

[@react.component]
let make = () => {
  <div className="Login"> <h1>{ "Login Page" |> ReasonReact.string }</h1> </div>;
};

```

ReasonReact comes with a built-in Router: ReasonReactRouter. The Router provides a hook, `useUrl()`, to consume the current url. To implement navigation, we just have to render a different page component based on the url path we consume from the `useUrl()` hook:

```ocaml
/* src/App.re */

[@react.component]
let make = () => {
  let url = ReasonReactRouter.useUrl();

  let page =
    switch (url.path) {
    | [] => <Home />
    | ["login"] => <Login />
    | _ => <div> {"Page not found :/" |> ReasonReact.string} </div>
    };

  <div className="App"> page </div>;
};
```

## I like 👍
- The built-in Router's API is pretty straight-forward.
- Pattern-matching over the url path will always remind you not to forget a fallback page, otherwise, it's not exhaustive.
- I can imagine that routing based on certain conditions can be implemented very easily and elegantly since routing is based on pattern-matching.


# Iteration 3: build the Login page interface
We've set up the navigation to the Home and Login pages, so let's put actual content in the Login page. To start, let's create a Login template with an email and password fields that store their values in the state:

```ocaml
/* src/Login.re */

[%bs.raw {|require('./Login.css')|}];

[@react.component]
let make = () => {
  let (email, setEmail) = React.useState(() => "");
  let (password, setPassword) = React.useState(() => "");

  let handleEmailChange = e => ReactEvent.Form.target(e)##value |> setEmail;
  let handlePasswordChange = e =>
    ReactEvent.Form.target(e)##value |> setPassword;

  let handleFormSubmit = e => {
    ReactEvent.Form.preventDefault(e);
    /* TODO: add logic for submitting the form */
  };

  <div className="Login">
    <h2> {"Login" |> ReasonReact.string} </h2>
    <form onSubmit=handleFormSubmit>
      <div className="inputField">
        <input
          type_="email"
          placeholder="Email"
          value=email
          onChange=handleEmailChange
          required=true
        />
      </div>
      <div className="inputField">
        <input
          type_="password"
          placeholder="Password"
          value=password
          onChange=handlePasswordChange
          required=true
        />
      </div>
      <Button title="Login" category=Button.PRIMARY>
        {"Login" |> ReasonReact.string}
      </Button>
    </form>
  </div>;
};

```

Here's how the Login page looks like:

<figure>
  <img src="https://thepracticaldev.s3.amazonaws.com/i/ebknfeeju7pwnr2fdxe5.png" alt="Login page">
  <figcaption>Login page</figcaption>
</figure>


## I like 👍
- I can say I became quite fast with ReasonReact, despite the few things I dislike and that I mentioned in the previous part (e.g. having to use ReasonReact.string and ReactEvent.Form.target).


# Iteration 4: add error handling
Before jumping into the form submit implementation, I want our component first to be able to handle potential errors. 

To do so, let's create a [Variant](https://reasonml.github.io/docs/en/variant#docsNav) that handles 3 possible errors:

- EMAIL_NOT_FOUND
- INVALID_PASSWORD
- OTHER (e.g. error when the user is blocked)

```ocaml
type loginError =
  | EMAIL_NOT_FOUND
  | INVALID_PASSWORD
  | OTHER;
```

Since server errors messages are strings, let's create a helper function that converts the string into a Variant:

```ocaml
let stringToLoginError = str =>
  switch (str) {
  | "EMAIL_NOT_FOUND" => Some(EMAIL_NOT_FOUND)
  | "INVALID_PASSWORD" => Some(INVALID_PASSWORD)
  | _ => None
  };
```

Now, we want to add an error field to our state. Since, at any point, we can either have an error or not, the field should be of type option(loginError).

Once we have that, it becomes pretty easy to conditionally render an email or password error using pattern-matching:

```ocaml
/* src/Login.re */

[%bs.raw {|require('./Login.css')|}];

type loginError =
  | EMAIL_NOT_FOUND
  | INVALID_PASSWORD
  | OTHER;

let stringToLoginError = str =>
  switch (str) {
  | "EMAIL_NOT_FOUND" => Some(EMAIL_NOT_FOUND)
  | "INVALID_PASSWORD" => Some(INVALID_PASSWORD)
  | _ => Some(OTHER)
  };

[@react.component]
let make = () => {
  let (email, setEmail) = React.useState(() => "");
  let (password, setPassword) = React.useState(() => "");
  let (error, setError) = React.useState(() => None);

  let handleEmailChange = e => ReactEvent.Form.target(e)##value |> setEmail;
  let handlePasswordChange = e =>
    ReactEvent.Form.target(e)##value |> setPassword;

  let handleFormSubmit = e => {
    ReactEvent.Form.preventDefault(e);
    /* TODO: add logic for submitting the form */
  };

  <div className="Login">
    <h2> {"Login" |> ReasonReact.string} </h2>
    <form onSubmit=handleFormSubmit>
      <div className="inputField">
        <input
          type_="email"
          placeholder="Email"
          value=email
          onChange=handleEmailChange
          required=true
        />
        {switch (error) {
         | Some(EMAIL_NOT_FOUND) =>
           <div className="error">
             {{js| ⚠  Email not found |js} |> ReasonReact.string}
           </div>
         | _ => ReasonReact.null
         }}
      </div>
      <div className="inputField">
        <input
          type_="password"
          placeholder="Password"
          value=password
          onChange=handlePasswordChange
          required=true
        />
        {switch (error) {
         | Some(INVALID_PASSWORD) =>
           <div className="error">
             {{js| ⚠ Invalid password |js} |> ReasonReact.string}
           </div>
         | _ => ReasonReact.null
         }}
      </div>
      <Button title="Login" category=Button.PRIMARY>
        {"Login" |> ReasonReact.string}
      </Button>
    </form>
  </div>;
};

```


## I like 👍
- Variants give confidence when dealing with different error types. By relying on them, you end up handling explicitly the different types of errors.


# Iteration 5: creating a wrapper for Fetch
When we submit the form, we want to make a login post request to the backend.
If there is an error with the user's credentials, well, we're ready to handle it 😎.
If the login is successful, we will just log a success message.

How do you make requests from the browser? You use the global function `fetch`.

I usually create a wrapper on top of fetch to make POST/GET requests for the following reasons:
- Avoid repetitive boilerplate.
- When the request fails, the fetch promise doesn't. So I usually check the status of the response and either resolve the promise or fail it based on it (c.f. the code below).

Here's how my fetch wrapper for post requests usually looks like in JavaScript:

```js
export async function post(url, body) {
  let response = await fetch(url, {
    method: 'POST',
    credentials: 'include',
    body: typeof body === 'string' ? body : JSON.stringify(body),
    headers: {
      'Content-Type': 'application/json'
    },
    mode: 'cors'
  });

  if (response.status >= 200 && response.status < 300) {
    return response;
  }

  throw response;
}
```

I can then use it anywhere else:

```js
try {
    const response = await post('some/url', { foo: "bar"});
    const jsonContent = await response.json();
} catch(e) {
    // do something with the error
}
```


Let's try to do the same in ReasonML. There are no built-in bindings to fetch in Reason, so we'll have to use [bs-fetch](https://github.com/reasonml-community/bs-fetch).

```ocaml
/* src/Request.re */

let post = (url, payload) => {
  let stringifiedPayload = payload |> Js.Json.object_ |> Js.Json.stringify;

  Fetch.fetchWithInit(
    url,
    Fetch.RequestInit.make(
      ~method_=Post,
      ~body=Fetch.BodyInit.make(stringifiedPayload),
      ~headers=Fetch.HeadersInit.make({"Content-Type": "application/json"}),
      (),
    ),
  );
};
```

The function fetchWithInit of the bs-fetch module Fetch is used to make a fetch call with configuration. We pass to it the url and a configuration we make with `Fetch.RequestInit.make()`.

So far, this returns a promise.

> Did I mention that I was using Firebase as a backend for authentication? Well, I am using Firebase as a backend for authentication.

Authentication errors returned by Firebase are JSON objects, so we have to use the `response.json()` method to extract the error object out of a response.

Thus, we want to extract the JSON content out of the fetch response. To do that, we can use the Fetch.Response.json function:

```ocaml
/* src/Request.re */

let post = (url, payload) => {
  let stringifiedPayload = payload |> Js.Json.object_ |> Js.Json.stringify;

  Fetch.fetchWithInit(
    url,
    Fetch.RequestInit.make(
      ~method_=Post,
      ~body=Fetch.BodyInit.make(stringifiedPayload),
      ~headers=Fetch.HeadersInit.make({"Content-Type": "application/json"}),
      (),
    ),
  ) |> Js.Promise.then_(Fetch.Response.json)
};

```

Since we will be using `then_` `catch`, `resolve`, and `reject` often, we can open the Js.Promise module so that we don't have to prepend them with Js.Promise at every call:

```ocaml
/* src/Request.re */

let post = (url, payload) => {
  let stringifiedPayload = payload |> Js.Json.object_ |> Js.Json.stringify;

  Js.Promise.(
    Fetch.fetchWithInit(
      url,
      Fetch.RequestInit.make(
        ~method_=Post,
        ~body=Fetch.BodyInit.make(stringifiedPayload),
        ~headers=Fetch.HeadersInit.make({"Content-Type": "application/json"}),
        (),
      ),
    )
    |> then_(Fetch.Response.json)
  );
};

```

Just like the JavaScript `json()` function, `Fetch.Response.json()` returns a promise that resolves with an object of type `Js.Json` (you can see that in the editor if you hover over it).

If we want to extract any data from an object of type `Js.Json`, we'll have to convert it into a `Js.Dict` (dictionary/map) object using the function Js.Json.decodeObject(). This returns an option, since the JSON response can be empty or simply not an object. Therefore, we will have to use pattern-matching to handle both cases and extract the `Js.Dict` value:

```ocaml
/* src/Request.re */

let post = (url, payload) => {
  let stringifiedPayload = payload |> Js.Json.object_ |> Js.Json.stringify;

  Js.Promise.(
    Fetch.fetchWithInit(
      url,
      Fetch.RequestInit.make(
        ~method_=Post,
        ~body=Fetch.BodyInit.make(stringifiedPayload),
        ~headers=Fetch.HeadersInit.make({"Content-Type": "application/json"}),
        (),
      ),
    )
    |> then_(Fetch.Response.json)
    |> then_(response => {
      switch (Js.Json.decodeObject(response)) {
        | Some(decodedRes) => {
          resolve(decodedRes);
        }
        | None => resolve(Js.Dict.empty())
      }
    })
  );
};
```

Now that we have the response object, we need to check if it has an error or not. If the content has an error object, we reject the Promise with an exception containing the error message. Therefore, we will have to create a custom exception type for that. Otherwise, we resolve the Promise with a `Js.Dict` object containing the response:


```ocaml
/* src/Request.re */

exception PostError(string);

let post = (url, payload) => {
  let stringifiedPayload = payload |> Js.Json.object_ |> Js.Json.stringify;

  Js.Promise.(
    Fetch.fetchWithInit(
      url,
      Fetch.RequestInit.make(
        ~method_=Post,
        ~body=Fetch.BodyInit.make(stringifiedPayload),
        ~headers=Fetch.HeadersInit.make({"Content-Type": "application/json"}),
        (),
      ),
    )
    |> then_(Fetch.Response.json)
    |> then_(response =>
         switch (Js.Json.decodeObject(response)) {
         | Some(decodedRes) =>
           switch (Js.Dict.get(decodedRes, "error")) {
           | Some(error) =>
             reject(
               PostError(
                 "There is an error, but we don't know its message yet",
               ),
             )

           | None => resolve(decodedRes)
           }
         | None => resolve(Js.Dict.empty())
         }
       )
  );
}
```

All that we have to do now is extract the error message from the Js.Json error, the same way we extracted the error object from the response. The only difference is that the message is a `string` and not an object:

```ocaml
/* src/Request.re */

exception PostError(string);

let post = (url, payload) => {
  let stringifiedPayload = payload |> Js.Json.object_ |> Js.Json.stringify;

  Js.Promise.(
    Fetch.fetchWithInit(
      url,
      Fetch.RequestInit.make(
        ~method_=Post,
        ~body=Fetch.BodyInit.make(stringifiedPayload),
        ~headers=Fetch.HeadersInit.make({"Content-Type": "application/json"}),
        (),
      ),
    )
    |> then_(Fetch.Response.json)
    |> then_(response =>
         switch (Js.Json.decodeObject(response)) {
         | Some(decodedRes) =>
           switch (Js.Dict.get(decodedRes, "error")) {
           | Some(error) =>
             switch (Js.Json.decodeObject(error)) {
             | Some(decodedErr) =>
               switch (Js.Dict.get(decodedErr, "message")) {
               | Some(errorMessage) =>
                 switch (Js.Json.decodeString(errorMessage)) {
                 | Some(decodedErrorMessage) =>
                   reject(PostError(decodedErrorMessage))
                 | None => reject(PostError("POST_ERROR"))
                 }
               | None => resolve(decodedRes)
               }
             | None => resolve(decodedRes)
             }

           | None => resolve(decodedRes)
           }
         | None => resolve(Js.Dict.empty())
         }
       )
  );
};
```

That's it, let's see in the next iteration whether our module works or not.

But before that, here's what I like/dislike:

## I like 👍
- Js.Promise always returns a Promise. This makes their returned value much more predictable than promises in JavaScript, which can either return a Promise or a value.

- Js.Post always returns a Promise that resolves with a Js.Dict. It's pretty hard to have an error by using it since its return type is predictable.

- Although it was a REAL struggle to extract an error message from the response, I have to admit that it was bullet-proof. I managed to make it work before even looking at the browser.

## I dislike 👎
- The syntax of the bs-fetch API is quite unnatural.
- Although accessing a JSON value is safe, it's undeniably a pain and easily results in a nesting hell that is very hard to read.

## Iteration #6: using the Request module in the Login page
We've built a nice module to make POST requests, let's use in the Login page.

Before doing that, we have to handle one issue: we can't directly access an exception from a rejected Promise. Let's follow the [official BuckleScript example](https://bucklescript.github.io/docs/en/interop-misc#safe-external-data-handling) and build a small helper function that extracts our exception from a rejected Promise:

```ocaml
  let handlePromiseFailure =
    [@bs.open]
    (
      fun
      | Request.PostError(err) => {
          err;
        }
    );
```

We can now make our POST calls in the `handleFormSubmit` function:


```ocaml
/* src/Login.re */

  let handleFormSubmit = e => {
    ReactEvent.Form.preventDefault(e);
    let payload = Js.Dict.empty();
    Js.Dict.set(payload, "email", Js.Json.string(email));
    Js.Dict.set(payload, "password", Js.Json.string(password));

    Js.Promise.(
      Request.post(loginUrl, payload)
      |> then_(res =>
           {
             alert("Login successful!");
             setError(_ => None);
           }
           |> resolve
         )
      |> catch(e =>
           (
             switch (handlePromiseFailure(e)) {
             | Some(err) => setError(_ => stringToLoginError(err))
             | None => setError(_ => None)
             }
           )
           |> resolve
         )
      |> ignore
    );
  };

```

To test the Login form, I created the following user:

```json
{ "email": "demo@demo.com", "password": "demodemo" }
```


And here's the final outcome:

<figure>
  <img src="https://thepracticaldev.s3.amazonaws.com/i/kqhk965ppigtor0dlppk.gif" alt="Final Login form">
  <figcaption>Final Login form</figcaption>
</figure>

That's it! The Login page works exactly as we wanted it to! 🎉

## I dislike 👎
The extra step I have to make to extract an exception from a rejected Promise.