Build a type safe React App with ReasonML, Part 1
Tags: react, tutorial, reason
Published: 2020-03-27 17:18:07

---
title: Build a type safe React App with ReasonML, Part 1
published: true
description: Using the power of ReasonML to build type safe React Applications
tags: react, tutorial, reasonml
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/dpt1cvylcjmvbipalyit.png
series: Build a type safe React App with ReasonML
---

Inspired by [this tutorial](https://dev.to/bnevilleoneill/how-to-build-a-type-safe-react-redux-app-jcm).

I wanted to showcase a real world project using ReasonML which is an ecosystem of tools and libraries for developing type safe code using OCaml in the Browser. My aim is to help you see that there are not many differences between ReasonML and plain Javascript as the type system is smart enough to perform type inference without being too explicit.

In this example two-part series we’ll create a sample e-commerce app like the one shown in the inspired article above.

Let's get started:

## Building a type-safe ReasonML app

We need to get started working with a ReasonML by configuring our project first.

First install the `bsb-platform` which is the ReasonML compiler tooling:

```bash
$ npm install -g bs-platform
```

Next create a new ReasonML project using the React Hooks theme which will setup the necessary boilerplate project for us:

```bash
$ bsb -init reason-example -theme react-hooks
```

The default boilerplate maybe not familiar for us. I recommend doing the following changes:

* Remove the following files:
```bash
indexProduction.html
watcher.js
UNUSED_webpack.config.js
```

* Change the `index.html` like this:

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>ReasonReact Examples</title>
</head>
<body>
  <div id="root"></div>
  <script src="Index.js"></script>
</body>
</html>
```

* Create a new `webpack.config.js` file with the following content:

```javascript
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const outputDir = path.join(__dirname, 'build/');

const isProd = process.env.NODE_ENV === 'production';

module.exports = {
  entry: './src/Index.bs.js',
  mode: isProd ? 'production' : 'development',
  output: {
    path: outputDir,
    filename: 'Index.js'
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'index.html',
      inject: false
    })
  ],
  devServer: {
    compress: true,
    contentBase: outputDir,
    port: process.env.PORT || 8000,
    historyApiFallback: true
  }
};
```

* Change the `bsconfig.json` file like this:
```json
{
  "name": "reason-react-example",
  "reason": {
    "react-jsx": 3
  },
  "sources": [{
    "dir" : "src",
    "subdirs" : true
  }],
  "bsc-flags": ["-bs-super-errors", "-bs-no-version-header"],
  "package-specs": [{
    "module": "commonjs",
    "in-source": true
  }],
  "suffix": ".bs.js",
  "namespace": true,
  "bs-dependencies": [
    "reason-react"
  ],
  "bs-dev-dependencies": ["@glennsl/bs-jest"],
  "refmt": 3,
  "gentypeconfig": {
    "language": "typescript",
    "module": "es6",
    "importPath": "relative",
    "debug": {
      "all": false,
      "basic": false
    }
  }
}

```

* Create a `babel.config.js` file with the following contents:

```javascript
module.exports = {
  env: {
    test: {
      plugins: ["transform-es2015-modules-commonjs"]
    }
  }
};
```

* Update the package.json so that it has the following contents:

```json
{
  "name": "reason-example",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "bsb -make-world",
    "start": "bsb -make-world -w",
    "clean": "bsb -clean-world",
    "webpack": "webpack -w",
    "webpack:production": "NODE_ENV=production webpack",
    "server": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
    "bs-platform": "^7.2.2",
    "gentype": "^3.15.0",
    "webpack-cli": "^3.3.11"
  },
  "dependencies": {
    "@glennsl/bs-jest": "^0.5.0",
    "bs-fetch": "^0.5.2",
    "html-webpack-plugin": "^3.2.0",
    "react": "^16.13.0",
    "react-dom": "^16.13.0",
    "reason-react": "^0.7.0",
    "webpack": "^4.42.0",
    "webpack-dev-server": "^3.10.3"
  },
  "jest": {
    "transformIgnorePatterns": [
      "/node_modules/(?!@glennsl/bs-jest|bs-platform).+\\.js$"
    ]
  }
}

```

* Finally install the npm dependencies:

```bash
$ npm i
```

If you want to test the application now you need to run the dev server and the bsb compiler in two tabs:

```bash
$ npm run start
// In another tab
$ npm run server
```

However for the example you should delete all the examples inside the `src` folder and keep an `Index.re` file with the following example code:

```reasonml
ReactDOMRe.renderToElementWithId(<App />, "root");
```
This is similar to React's `ReactDOM.render` method but a little bit more convenient.

Create a new file named `App.re` in the same folder and add the following code:

```reasonml
[@react.component]
let make = () => {
  <main> {"Hello From ReasonML" |> React.string} </main>;
};
```

Let's explain here some conventions:

* We use the `[@react.component]` annotation to specify that it's a react component
* We name a let binding as `make` so that by default ReasonReact will discover it
* We use regular JSX but when we want to display a string we need to pipe it to the appropriate type. In that case `|> React.string`.

Every-time you change anything in the code it will reload and see the changes to the UI.

## Routing

ReasonReact comes with a router! Let's add the first route to match the home page:

Create a new file named `Routes.re` and add the following code:

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

  switch (url.path) {
  | [] => <Home />
  | _ => <NotFound />
  };
};
```

This will match either the base path `/` rendering the `Home` component or anything else rendering the `NotFound` component.

Create the following components:

**Home.re**
```reasonml
[@react.component]
let make = () => {
  <main> {"Hello World  " |> React.string} </main>;
};
```

**NotFound.re**
```reasonml
[@react.component]
let make = () => {
  <main> {"404 Page not found!" |> React.string} </main>;
};
```

Finally update the `App` component to render the `Routes` instead:

**App.re**
```reasonml
[@react.component]
let make = () => {
  <Routes />;
};
```

Now you know how to handle routing.

## Styles and images

We can add stylesheets and images using regular `require` imports. We just need to define some external helpers that will map from ReasonML to Javascript.

Create a new file named `Helpers.re` and add the following code:

```reasonml
/* require css file */
[@bs.val] external requireCSS: string => unit = "require";

/* require an asset (eg. an image) and return exported string value (image URI) */
[@bs.val] external requireImage: string => string = "require";
```

So whenever we want to include css files we use it like:

```reasonml
requireCSS('./styles.css');
```
and this will compile as:

```javascript
require('./styles.css');
```

Let's add styles for the `NotFound` page:

**NotFound.css**
```css
.NotFound {
    margin: 30px auto;
    display: flex;
    align-items: center;
    flex-direction: column;
}
.NotFound--image {
    margin-top: 60px;
}
```

Change the `NotFound.re` component to import the styles:

```reasonml
open Helpers;

requireCSS("./NotFound.css");

let notFoundImage = requireImage("./notFound.png");

[@react.component]
let make = () => {
  <main className="NotFound">
    <div className="NotFound--Image">
      <img src=notFoundImage alt="Not Found Image" />
    </div>
  </main>;
};
```

Finally you need to install the webpack dependencies and update the `webpack.config`:

```bash
$ npm i style-loader css-loader file-loader --save-dev
```

**webpack.config.js**
```javascript
...
module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif)$/i,
        loader: 'file-loader',
        options: {
          esModule: false,
        },
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader'
          },
          {
            loader: 'css-loader',
            options: {
              sourceMap: true
            }
          }
        ]
      },
    ]
```

You need to find a `notFound.png` image and place it inside the `src` folder. Once you run the application again you can see the not found page:

## Modelling the Domain problem

We have two important domains in the wireframe, inventory and cart:

![Diagram Showing Inventory and Cart Domains](https://dev-to-uploads.s3.amazonaws.com/i/lq6kumm174p2mh5e5ong.png)

We will create the application store and structure it based on the domain.

Let’s start with the inventory domain. 

### Inventory domain

ReasonReact has full support for React Hooks!. We can use reducers, effects, state, context variables to handle our application state. Let's start by defining our model types for the inventory domain based on the class diagram above.

Create a new file named `InventoryData.re` and add the following code:

```reasonml
type inventory = {
  id: string,
  name: string,
  price: int,
  image: string,
  description: string,
  brand: option(string),
  stockCount: int,
};

type action =
  | Fetch
  | FetchSuccess(list(inventory))
  | FetchError(string);

type state = {
  isLoading: bool,
  data: list(inventory),
  error: option(string),
};

let initialState = {isLoading: false, data: [], error: None};
```

The above code contains state, action types, and inventory domain mode
A few notes about the code above:

The `inventory` type determines the specified domain data
The `actions` variant determines the action types
The `state` handles the type of domain state. We also define an `initialState`

Now, it’s time to create an action for fetching the inventory store. Create a new file named `InventoryActions.re` with the following contents:

```reasonml
let fetchInventory = dispatch => {
  dispatch(InventoryData.Fetch);
  InventoryApi.fetch(payload =>
    dispatch(InventoryData.FetchSuccess(payload))
  )
  |> ignore;
};
```

The `InventoryApi.re` file contains the following content:

```reasonml
let fetch = callback => {
  callback(MockData.inventory);
};
```

Finally the `MockData.re` file is just a hardcoded list of inventory items:

```reasonml
open InventoryData;

let inventory = [
  {
    name: "Timber Gray Sofa",
    price: 1000,
    image: "../images/products/couch1.png",
    description: "This is a Test Description",
    brand: Some("Jason Bourne"),
    stockCount: 4,
    id: "fb94f208-6d34-425f-a3f8-e5b87794aef1",
  },
  {
    name: "Carmel Brown Sofa",
    price: 1000,
    image: "../images/products/couch5.png",
    description: "This is a test description",
    brand: Some("Jason Bourne"),
    stockCount: 2,
    id: "4c95788a-1fa2-4f5c-ab97-7a98c1862584",
  },
...
```

The final part of the inventory store is the reducer. Let's create that file:

**InventoryReducer.re**
```reasonml
open InventoryData;

let reducer: (state, action) => state =
  (state, action) =>
    switch (action) {
    | Fetch => {...state, isLoading: true}
    | FetchSuccess(data) => {...state, isLoading: false, data}
    | FetchError(error) => {...state, isLoading: false, error: Some(error)}
    };
```

Here we included the `InventoryData` module so that the types are inferred without prefixing the module name. Note that we can ignore the type definition of the reducer without losing type checking. ReasonML is always on guard if something goes wrong with the types!.

### Cart domain

It’s time to implement the types and actions for the cart model. The functionalities of the cart domain are similar to those of the inventory domain.

First, create a file named `CartData.re` and add the following code:

```reasonml
open InventoryData;

type cart = {
  id: string,
  items: list(inventory),
};

type action =
  | AddToCart(inventory)
  | RemoveFromCart(inventory)
  | Fetch
  | FetchSuccess(option(cart))
  | FetchError(string);

type state = {
  isLoading: bool,
  data: cart,
  error: option(string),
};

let initialState = {isLoading: false, data: {id: "1", items: []}, error: None};
```

This represents the cart domain attributes, cart action types, and cart state.

Next, create `CartActions.re` for the cart domain:
```reasonml
let fetchCart = dispatch => {
  dispatch(CartData.Fetch);
  CartApi.fetch(payload => dispatch(CartData.FetchSuccess(payload)))
  |> ignore;
};

let addToCart = (inventory, dispatch) => {
  dispatch(CartData.AddToCart(inventory)) |> ignore;
};

```

Where `CartApi.re` is:

```reasonml
let fetch = callback => {
  callback(MockData.cart);
};
```

Finally, write the code for the cart domain reducer. Create a file, name it `CartReducer.re`, and add the following code:

```reasonml
open CartData;

let reducer: (CartData.state, CartData.action) => CartData.state =
  (state, action) =>
    switch (action) {
    | Fetch => {...state, isLoading: true}
    | FetchSuccess(data) => {...state, isLoading: false, data}
    | FetchError(error) => {...state, isLoading: false, error: Some(error)}
    | AddToCart(inventory) =>
      let updatedInventory = [inventory, ...state.data.items];
      {
        ...state,
        isLoading: true,
        data: {
          id: state.data.id,
          items: updatedInventory,
        },
      };
    | RemoveFromCart(inventory) =>
      let updatedInventory =
        List.filter(
          (item: InventoryData.inventory) => item.id != inventory.id,
          state.data.items,
        );
      {
        ...state,
        isLoading: true,
        data: {
          id: state.data.id,
          items: updatedInventory,
        },
      };
    };

```

## Next Part

We will continue in the next and final part of this tutorial by defining the view components and glue everything together.