ReasonML for production React Apps? 🤔 (Part 3)
Tags: reason, react, javascript
Published: 2019-05-11 18:22:13

---
title: ReasonML for production React Apps? 🤔 (Part 3)
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-3/
---

A while ago I published [this](https://levelup.gitconnected.com/how-to-create-an-accessible-react-modal-5b87e6a27503) article about building an accessible and reusable modal/dialog component in React. 

<figure>
  <img src="https://thepracticaldev.s3.amazonaws.com/i/ja0duaz4tejuxqr3yadt.gif" alt="React modal">
  <figcaption>React modal</figcaption>
</figure>

The component achieves the following requirements:

- **A reusable component API**: we should be able to easily use our accessible modal anywhere and populate it with content.
- **Accessible markup.**
- **We should be able to open & close the modal just using the keyboard.**
- **Trap the focus in the modal:** since the modal is an inert component, the keyboard navigation should be trapped inside of it once it’s open.


The component makes use of React features like the Context, Portals, and Ref. To evaluate whether ReasonReact is ready for production, I will build the same modal in Reason and report what I like/dislike.

I will be doing that once again in iterations. You can find the final source code [here](https://github.com/siffogh/reason-react-modal). There is a separate branch for each iteration.


# Iteration #1: the modal as a simple div

<figure>
  <img src="https://thepracticaldev.s3.amazonaws.com/i/q8h622eucw20hvbu7owf.gif" alt="Iteration #1: the modal as a simple div">
  <figcaption>Iteration #1: the modal as a simple div</figcaption>
</figure>


## The modal component

First, let's create the modal component:

```ocaml
/* src/Modal.re */

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

[@bs.scope "document"] [@bs.val] external body: Dom.element = "body";

[@react.component]
let make = (~children) =>
  ReactDOMRe.createPortal(
    <div className="modal-container" role="dialog" ariaModal=true>
      <div className="modal-content"> children </div>
    </div>,
    body,
  );
```


To make the modal accessible, it should be isolated from the main application content. Using the `createPortal` function of ReactDOMRe, the modal is rendered directly in the body of the document. 

But first, we need to access the body...

Reason ships with a `Dom` module. Unfortunately, the module only contains useful types in it and doesn't provide bindings to the actual DOM.  

We can of course just use raw JavaScript to access `document.body` and type the value using the Dom module:

```ocaml
let body: Dom.element = [%raw "document.body"];
```

If we look at the compiled `Modal.bs.js` file, we'll see this totally unnecessary line:

```js
var body = (document.body);
```

Can you imagine how the compiled code would look like if we start using this approach whenever we want to access a JavaScript value? We can do better than that!

To access the body of the document, we can use BuckleScript's [external feature](https://bucklescript.github.io/docs/en/bind-to-global-values):

```ocaml
[@bs.scope "document"] [@bs.val] external body: Dom.element = "body";
```
We simply tell BuckleScript to give us the value `body` of type `Dom.element` present in the global value `document`. This syntax is pretty smart and efficient. If we look at the compiled `Modal.bs.js` file, we'll see that the body is accessed directly from the document object, the same way we would do it in JavaScript!


## Opening the modal

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

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

[@react.component]
let make = () => {
  let (isModalVisible, setIsModalVisible) = React.useState(() => false);
  <div className="App">
    <h1> {"Parent container" |> ReasonReact.string} </h1>
    <h3> {"This is just a demo container" |> ReasonReact.string} </h3>
    <button onClick={_ => setIsModalVisible(_ => true)}>
      {"open modal" |> ReasonReact.string}
    </button>
    {!isModalVisible
       ? ReasonReact.null : <Modal> {"Foo" |> ReasonReact.string} </Modal>}
  </div>;
};
```

To view/hide the modal, it's rendered in the App component conditionally based on the `isModalVisible` boolean state value.

The "open modal" button simply sets that value to `true`, opening the modal.


## I like 👍
- Using portals is pretty simple.
- BuckleSript external syntax makes accessing a DOM element simple and efficient.

## I dislike 👎
- The absence of actual bindings to the DOM. It would have made accessing the body of the document as easy as it is in JavaScript while benefiting from typing.



# Iteration #2: adding close buttons

<figure>
  <img src="https://thepracticaldev.s3.amazonaws.com/i/4vc4tjk7dk4bga6t4kph.png" alt="Iteration #2: adding close buttons">
  <figcaption>Iteration #2: adding close buttons</figcaption>
</figure>


We want to have 2 buttons to close the modal:
- A cross button in the header.
- A "Close" button in the footer.

Before doing that, we need first have the modal expose:
- A header component that contains the cross closing button and any elements we wish to render in the header of the modal.

- A body component that contains any elements we wish to render in the body of the modal.

- A footer component that will contain the second closing button and any elements we wish to render in the footer of the modal.

```ocaml
/* src/Modal.re */

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

[@bs.scope "document"] [@bs.val] external body: Dom.element = "body";

module Cross = {
  [@bs.module "./cross.svg"] [@react.component]
  external make: unit => React.element = "default";
};

[@react.component]
let make = (~children) => {
  ReactDOMRe.createPortal(
    <div className="modal-container" role="dialog" ariaModal=true>
      <div className="modal-content"> children </div>
    </div>,
    body,
  );
};

module Header = {
  [@react.component]
  let make = (~children) => {
    <div className="modal-header">
      children
      <button className="cross-btn" title="close modal"> <Cross /> </button>
    </div>;
  };
};

module Body = {
  [@react.component]
  let make = (~children) => <div className="modal-body"> children </div>;
};

module Footer = {
  [@react.component]
  let make = (~children) => <div className="modal-footer"> children </div>;

  module CloseBtn = {
    [@react.component]
    let make = (~children) => {
      <button className="close-btn" title="close modal"> children </button>;
    };
  };
};
```

We can then refactor our App.re and make use of the new modal sub-components:

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

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

[@react.component]
let make = () => {
  let (isModalVisible, setIsModalVisible) = React.useState(() => false);

  <div className="App">
    <h1> {"Parent container" |> ReasonReact.string} </h1>
    <h3> {"This is just a demo container" |> ReasonReact.string} </h3>
    <button onClick={_ => setIsModalVisible(_ => !isModalVisible)}>
      {"open modal" |> ReasonReact.string}
    </button>
    {!isModalVisible
       ? ReasonReact.null
       : <Modal>
           <Modal.Header> {"Header" |> ReasonReact.string} </Modal.Header>
           <Modal.Body> {"Body" |> ReasonReact.string} </Modal.Body>
           <Modal.Footer>
             <Modal.Footer.CloseBtn>
               {"Close" |> ReasonReact.string}
             </Modal.Footer.CloseBtn>
           </Modal.Footer>
         </Modal>}
  </div>;
};
```

## I like 👍
- Reason's module syntax is pretty simple and creating sub-modules is effortless.
 

# Iteration #3: Closing the modal
Just like opening the modal, closing it can be done by setting `isModalVisible` in App.re to `true`.

Let's pass to the Modal a function, `onModalClose`, that does exactly that:

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


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

[@react.component]
let make = () => {
  let (isModalVisible, setIsModalVisible) = React.useState(() => false);

  <div className="App">
    <h1> {"Parent container" |> ReasonReact.string} </h1>
    <h3> {"This is just a demo container" |> ReasonReact.string} </h3>
    <button onClick={_ => setIsModalVisible(_ => !isModalVisible)}>
      {"open modal" |> ReasonReact.string}
    </button>
    {!isModalVisible
       ? ReasonReact.null
       : <Modal onModalClose={() => setIsModalVisible(_ => false)}>
           <Modal.Header> {"Header" |> ReasonReact.string} </Modal.Header>
           <Modal.Body> {"Body" |> ReasonReact.string} </Modal.Body>
           <Modal.Footer>
             <Modal.Footer.CloseBtn>
               {"Close" |> ReasonReact.string}
             </Modal.Footer.CloseBtn>
           </Modal.Footer>
         </Modal>}
  </div>;
};
```

By now, the modal component has a prop function to close the Modal. The only thing left is to make it available to the header and footer close buttons.

To do that we can expose it in a React Context Provider. Any component that needs to access the `onModalClose` function can then consume it using a `useContext` hook.

To create a context for the modal, we can use React `createContext` function and pass to it an initial value. Since the `onModalClose` function is of type `unit => unit`, the initial value needs to be of the same type:


```ocaml
let modalContext = React.createContext(() => ());
```

The provider can't be accessed directly from the modal's context. One way of using it is to create a provider module which exposes a `make` and `makeProps` functions:

```ocaml
module ContextProvider = {
  let makeProps = (~value, ~children, ()) => {
    "value": value,
    "children": children,
  };

  let make = React.Context.provider(modalContext);
};
```

We can then wrap the modal's children inside the context provider and consume our value in the header and footer:


```ocaml
/* src/Modal.re */

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

[@bs.scope "document"] [@bs.val] external body: Dom.element = "body";

module Cross = {
  [@bs.module "./cross.svg"] [@react.component]
  external make: unit => React.element = "default";
};

let modalContext = React.createContext(() => ());

module ContextProvider = {
  let makeProps = (~value, ~children, ()) => {
    "value": value,
    "children": children,
  };

  let make = React.Context.provider(modalContext);
};

[@react.component]
let make = (~children, ~onModalClose) => {
  ReactDOMRe.createPortal(
    <div className="modal-container" role="dialog" ariaModal=true>
      <div className="modal-content">
        <ContextProvider value=onModalClose> children </ContextProvider>
      </div>
    </div>,
    body,
  );
};

module Header = {
  [@react.component]
  let make = (~children) => {
    let onModalClose = React.useContext(modalContext);

    <div className="modal-header">
      children
      <button
        className="cross-btn"
        title="close modal"
        onClick={_ => onModalClose()}>
        <Cross />
      </button>
    </div>;
  };
};

module Body = {
  [@react.component]
  let make = (~children) => <div className="modal-body"> children </div>;
};

module Footer = {
  [@react.component]
  let make = (~children) => <div className="modal-footer"> children </div>;

  module CloseBtn = {
    [@react.component]
    let make = (~children) => {
      let onModalClose = React.useContext(modalContext);

      <button
        className="close-btn"
        title="close modal"
        onClick={_ => onModalClose()}>
        children
      </button>;
    };
  };
};

```

## I like 👍
- I was able to use the Context to implement the feature, even if it was a workaround.
- After getting stuck with using the Context, I managed to get a working solution in no-time through the [forum](https://reasonml.chat/t/current-state-of-react-context/1626/2) and [discord channel](https://discordapp.com/channels/235176658175262720/235199093155168257).

## I dislike 👎
- There is no documentation about using the context.
- The solution I used is undoubtedly a hack.


# Iteration #4: closing the modal through a keyboard shortcut

<figure>
  <img src="https://thepracticaldev.s3.amazonaws.com/i/dq8ohaqee3i3mxm1jyjw.gif" alt="Iteration #4: closing the modal through a keyboard shortcut">
  <figcaption>Iteration #4: closing the modal through a keyboard shortcut</figcaption>
</figure>

We want to be able to close the modal by pressing the `ESCAPE` key. To do so, we need to use the external syntax to access 2 functions:
- `document.addEventListener` (to add a listener).
- `document.removeEventListener` (to remove a listener).

We have to be explicit about the type of event used in both functions. The `ReactEvent` module provides types for various events. In our case, we are dealing with events of type `Keyboard`. Since our functions only deal with keyboard events, I decided to name them a bit differently:


```ocaml
[@bs.scope "document"] [@bs.val]
external addKeybordEventListener:
  (string, ReactEvent.Keyboard.t => unit) => unit =
  "addEventListener";

[@bs.scope "document"] [@bs.val]
external removeKeybordEventListener:
  (string, ReactEvent.Keyboard.t => unit) => unit =
  "removeEventListener";
```

We can then use the 2 functions to create a `keydown` listener that reacts to the `ESCAPE` key:

```ocaml
let keyDownListener = e =>
  if (ReactEvent.Keyboard.keyCode(e) === 27) {
     onModalClose();
  };
```

The `useEffect` hook can be used to create a function to subscribe to our `keyDownListener`. The function returns than another function to clean up the effect, wrapped in an option:

```ocaml
/* src/Modal.re */

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

[@bs.scope "document"] [@bs.val] external body: Dom.element = "body";

[@bs.scope "document"] [@bs.val]
external addKeybordEventListener:
  (string, ReactEvent.Keyboard.t => unit) => unit =
  "addEventListener";

[@bs.scope "document"] [@bs.val]
external removeKeybordEventListener:
  (string, ReactEvent.Keyboard.t => unit) => unit =
  "removeEventListener";

module Cross = {
  [@bs.module "./cross.svg"] [@react.component]
  external make: unit => React.element = "default";
};

let modalContext = React.createContext(() => ());

module ContextProvider = {
  let makeProps = (~value, ~children, ()) => {
    "value": value,
    "children": children,
  };

  let make = React.Context.provider(modalContext);
};

[@react.component]
let make = (~children, ~onModalClose) => {
  let keyDownListener = e =>
    if (ReactEvent.Keyboard.keyCode(e) === 27) {
      onModalClose();
    };

  let effect = () => {
    addKeybordEventListener("keydown", keyDownListener);
    Some(() => removeKeybordEventListener("keyDown", keyDownListener));
  };

  React.useEffect(effect);

  ReactDOMRe.createPortal(
    <div className="modal-container" role="dialog" ariaModal=true>
      <div className="modal-content">
        <ContextProvider value=onModalClose> children </ContextProvider>
      </div>
    </div>,
    body,
  );
};

module Header = {
  [@react.component]
  let make = (~children) => {
    let onModalClose = React.useContext(modalContext);

    <div className="modal-header">
      children
      <button
        className="cross-btn"
        title="close modal"
        onClick={_ => onModalClose()}>
        <Cross />
      </button>
    </div>;
  };
};

module Body = {
  [@react.component]
  let make = (~children) => <div className="modal-body"> children </div>;
};

module Footer = {
  [@react.component]
  let make = (~children) => <div className="modal-footer"> children </div>;

  module CloseBtn = {
    [@react.component]
    let make = (~children) => {
      let onModalClose = React.useContext(modalContext);

      <button
        className="close-btn"
        title="close modal"
        onClick={_ => onModalClose()}>
        children
      </button>;
    };
  };
};
```

## I like 👍
- useEffect being fully typed, it reminds you to clean up the effect. You either explicitly return `None` or a cleanup function. This avoids memory leaks when adding listeners and not removing them.

## I dislike 👎
- There is no documentation about the use of `useEffect`.


# Iteration #5: Trapping the focus in the modal
There is one more thing left in order for our modal to be properly accessible: the focus inside of it should be trapped. Once the modal is opened, we should focus the first focusable element in it. From then, pressing the TAB or SHIFT + TAB keys will only allow the user to navigate inside the modal. 

<figure>
  <img src="https://thepracticaldev.s3.amazonaws.com/i/adiro5t2jje42hglxwm3.jpeg" alt="Trapping the focus inside the modal">
  <figcaption>Trapping the focus inside the modal</figcaption>
</figure>


We need to first create a Ref to access the modal dom element:

```ocaml
let modalRef = React.useRef(Js.Nullable.null);
```

Notice that we have to initiate `useRef` with a null value through the `Js.Nullable` module. Fortunatelty, it's pretty easy to convert from a `Js.Nullable` to an option:

```ocaml
switch(Js.Nullable.toOption(someNullable)) {
    | Some(value) => ...
    | None => ...
}
```
 
To select all focusable elements inside the modal, we can use the `querySelector` function of the DOM element inside the Ref to the modal component.

Due to the absence of proper bindings to the DOM, we either have to write our own bindings, use some 3rd party library, or just use `ReactDOMRe.domElementToObj` which converts the DOM element inside the modal ref into an object. 

For the time being, I choose the 3rd option. This allows us to access the `querySelector` function easily but lose track of all types:

```ocaml
/* src/Modal.re */

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

[@bs.scope "document"] [@bs.val] external body: Dom.element = "body";

[@bs.scope "document"] [@bs.val]
external addKeybordEventListener:
  (string, ReactEvent.Keyboard.t => unit) => unit =
  "addEventListener";

[@bs.scope "document"] [@bs.val]
external removeKeybordEventListener:
  (string, ReactEvent.Keyboard.t => unit) => unit =
  "removeEventListener";

[@bs.scope "document"] [@bs.val]
external activeElement: Dom.element = "activeElement";

module Cross = {
  [@bs.module "./cross.svg"] [@react.component]
  external make: unit => React.element = "default";
};

let modalContext = React.createContext(() => ());

module ContextProvider = {
  let makeProps = (~value, ~children, ()) => {
    "value": value,
    "children": children,
  };

  let make = React.Context.provider(modalContext);
};
[@react.component]
let make = (~children, ~onModalClose) => {
  let modalRef = React.useRef(Js.Nullable.null);

  let handleTabKey = e => {
    let current = React.Ref.current(modalRef);
    switch (Js.Nullable.toOption(current)) {
    | Some(element) =>
      let elementObj = ReactDOMRe.domElementToObj(element);
      let elements =
        elementObj##querySelectorAll(
          "a[href], button, textarea, input[type='text'], input[type='radio'], input[type='checkbox'], select",
        );
      let firstElement = elements[0];
      let lastElement = elements[elements##length - 1];

      if (!ReactEvent.Keyboard.shiftKey(e) && activeElement !== firstElement) {
        firstElement##focus();
        ReactEvent.Keyboard.preventDefault(e);
      };

      if (ReactEvent.Keyboard.shiftKey(e) && activeElement !== lastElement) {
        lastElement##focus();
        ReactEvent.Keyboard.preventDefault(e);
      };

    | None => ignore()
    };
  };

  let keyListenersMap =
    Js.Dict.fromArray([|("27", _ => onModalClose()), ("9", handleTabKey)|]);

  let effect = () => {
    let keyDownListener = e => {
      let keyCodeStr = ReactEvent.Keyboard.keyCode(e) |> string_of_int;

      switch (Js.Dict.get(keyListenersMap, keyCodeStr)) {
      | Some(eventListener) => eventListener(e)
      | None => ignore()
      };
    };

    addKeybordEventListener("keydown", keyDownListener);
    Some(() => removeKeybordEventListener("keyDown", keyDownListener));
  };

  React.useEffect(effect);

  ReactDOMRe.createPortal(
    <div className="modal-container" role="dialog" ariaModal=true>
      <div className="modal-content" ref={ReactDOMRe.Ref.domRef(modalRef)}>
        <ContextProvider value=onModalClose> children </ContextProvider>
      </div>
    </div>,
    body,
  );
};

module Header = {
  [@react.component]
  let make = (~children) => {
    let onModalClose = React.useContext(modalContext);

    <div className="modal-header">
      children
      <button
        className="cross-btn"
        title="close modal"
        onClick={_ => onModalClose()}>
        <Cross />
      </button>
    </div>;
  };
};

module Body = {
  [@react.component]
  let make = (~children) => <div className="modal-body"> children </div>;
};

module Footer = {
  [@react.component]
  let make = (~children) => <div className="modal-footer"> children </div>;

  module CloseBtn = {
    [@react.component]
    let make = (~children) => {
      let onModalClose = React.useContext(modalContext);

      <button
        className="close-btn"
        title="close modal"
        onClick={_ => onModalClose()}>
        children
      </button>;
    };
  };
};
```

## I like 👍
- Being able to convert from a `Js.Nullable` to an option.
- I was able to implement the feature regardless of the absence of proper bindings to the DOM.

## I dislike 👎
- Not being able to use an option with the useRef hook.
- The absence of proper bindings to the DOM.