ReasonReact: Empower your Frontend with ReasonML

A step by step guide to create your first Reason-React component

Prateek Pandey
Porter Blog

--

image-credits: Rahul Raveendran

“Work smarter, Not harder” is quite a popular and pragmatic quote. The whole idea behind the statement is to get your work done with maximum efficiency. And this holds true in all facets of our life.

At Porter Tech, we consistently strive to leverage the best available technology to make our product lifecycle lean, smart and efficient. This enables us to give maximum value to our users. Moreover, it puts our engineering team at the forefront of the technological evolution.

The whole idea of using ReasonML for frontend web development stems from the experience of our mobile team who were quite successful in implementing RIBs, a modular and highly scalable cross-platform mobile architecture framework. To follow a similar architecture philosophy on the web and keep consistency across all engineering teams, we needed a statically typed programming language. Hence JavaScript was ruled out.Typescript seemed a reasonable choice, but ReasonML proved way too powerful. ReasonML is backed by the mature OCaml community and is the language for writing React. As a matter of fact, Reason’s creator also created ReactJS, whose first prototypes were written in SML, a distant cousin of OCaml.

ReasonML is the language behind Reason-React framework and is quickly gaining traction as a programming language. It provides decent interoperability with javascript ecosystem and has a nice growing community on Discord to support developers in all timezones.

In the following post, we explore the smartness of Reason-React framework for building a modern frontend web application. Reason-React provides a safer, simpler way to create React applications in ReasonML. I encourage you to have a basic understanding of React for gaining most out of this blogpost.

For demonstration purpose, we create a simple application which fetches a list of posts and displays them as a list. Please find a general overview of a typical reason-react application below:-

module <Module Name> {
type action =
| <use case 1>
| <use case 2>
type state = {
<a reason record type defining the application state>
}

let component = ReasonReact.reducerComponent(<component-name>);

let make = (_children) => {
...component,
initialState: () => {
<initial value of application state>
},
didMount: self => {
<business logic to be implemented just after the component is mounted on DOM>
},
reducer: (action, _state) => {
| <use case 1 implementation>
| <use case 2 implementation>
},
render: ({state}) => {
<To be rendered on DOM according to application state>
}
}
}

Lets create the application, step by step:

  1. Component Creation: The first step is to instantiate a reason-react component. We use ReasonReact.reducerComponent API for creating a stateful component in Reason-React, which is similar to React’s stateful components.
    The API creates a plain reason record, whose fields like didMount, state, initialState, render etc we can override. This component record is what make function (as shown below) asks us to return. Under the hood, the make function is invoked while creating a Reason-React element. We don’t need to worry about it, since almost all the time we use ReasonReact’s JSX to write our Reason-React components.
let component = ReasonReact.reducerComponent("PostsComponent");

let make = (_children) => {
...component,
render: () {
}
}

2. Props: The component only accepts a default _children prop. The _ is used to indicate to Reason compiler that the variable children is unused.

3. State: Lets’ define the application state. The state has a field named posts which is of type loadable. This will store the list of posts fetched.

type state = {
posts: Loadable.loadable(Posts.posts)
}

Loadable is a variant. Variant is a union of data types which come together for solving a specific use-case. For e.g. here the loadable variant is used for tracking the state of the application like Loading, Live etc. Also, during Live state, it stores data fetched from the API as a variant payload.

Type definition for the variant loadable:-

module Loadable = {  type loadable('t) =    |  Init    |  Loading    |  Live('t)
}

Here `t in ReasonML is akin to generic types in other languages. For the current scenario the value of `t is Posts.posts.

Did you observe, how a combination of ReasonML variants and generic types gives a powerful Loadable type which can be used across applications to define state of reason-react components.

4. Initial State: Initially the posts field of the component state is assigned a value Init which is one of the possible values the variant loadable can hold. This is the state of the component when it is mounted on the DOM.

initialState: () => {
posts: Loadable.Init
},

5. Actions:

As described in Reason React documentation:

It’s a variant of all the possible state transitions in your component.

We define two actions for triggering our state updates :-

type action =
| LoadData
| OnLoadedData(Posts.posts)

6. DidMount: This react lifecycle method is called once during the lifetime of the application. It is invoked after the component’s initial render on the DOM. This is a good place to put the trigger point for the action LoadData. As discussed above, this action invokes the reducer use-case to change the state from Loadable.Init to Loadable.Loading and make the API call for fetching the posts.

didMount: self => {
self.send(LoadData)
},

7. Reducer:

As aptly put in Reason React documentation:

ReasonReact stateful components are like ReactJS stateful components, except with the concept of “reducer” (like Redux) built in.

The reducer pattern-matches on the possible actions and specifies what state update each action corresponds to.

There are two reducers corresponding to the above defined actions.

a.) LoadData: It updates the state to Loadable. Loading and makes the API call to fetch posts as a side-effect. We use ReasonReact.UpdateWithSideEffects API to achieve this. Once the API call succeeds, the control comes to the then block of the Javascript Promise, which is a good place to act as a trigger point for the action OnLoadedData. The action payload is the resolved response from the API.

LoadData =>
ReasonReact.UpdateWithSideEffects(
Loading,
(
self =>
Js.Promise.(
Network.fetchPosts()
|> then_((result) => resolve(self.send(OnLoadedData(result))))
|> ignore
)
)
)

b.) OnLoadedData: This updates the application state to Loadable.Success. Here we perform pure state update, hence the use of
ReasonReact.Update API.

| OnLoadedData(data) => ReasonReact.Update(
{posts: Loadable.Live(data)})

8. Render: Finally onto render. This is where the variants, pattern matching and static type system, all the crown jewels of ReasonML come together to create a beautiful yet powerful Reason React component. Here we can pattern match posts field of the state against all the values it might possibly possess and accordingly render the corresponding JSX.

render: ({state}) => {
switch(state.posts) {
| Init => <div>(ReasonReact.string("Init"))</div>
| Loading => <div>(ReasonReact.string("Loading"))</div>
| Live(posts) => <PostsViewComponent posts />
}

PostsViewComponent ( as shown below) is a stateless component where actual rendering of the posts happen. Notice, as contrary to PostsComponent which is a stateful component, we use ReasonReact.statelessComponent API for creating PostsViewComponent.

module PostsViewComponent {
let component = ReasonReact.statelessComponent("PostsViewComponent")

let make = (~posts: Posts.posts, _children) => {
...component,
render: (_self) => {
<li>(
posts
|> List.map((post: Posts.post) => {
<ul key=(string_of_int(post.id))>
<h1>(ReasonReact.string(post.title))</h1>
</ul>
})
|> Array.of_list
|> ReasonReact.array
)
</li>
}
}
}

And we have successfully created a Reason-React application that fetches posts and renders them on the screen.

The complete code for the above implementation can be found below:-

module PostsComponent {
type action =
| LoadData
| OnLoadedData(Posts.posts)

type state = {
posts: Loadable.loadable(Posts.posts)
}

let component = ReasonReact.reducerComponent("PostsComponent");

let make = (_children) => {
...component,
initialState: () => {
posts: Loadable.Init
},
didMount: self => {
self.send(LoadData)
},
reducer: (action, _state) => {
switch(action) {
| LoadData => ReasonReact.UpdateWithSideEffects(
{posts: Loadable.Loading},
self =>
Js.Promise.(
fetchPosts()
|> then_(result => resolve(self.send(OnLoadedData(result))))
|> ignore
)
)
| OnLoadedData(data) => ReasonReact.Update({posts: Loadable.Live(data)})
}
},
render: ({state}) => {
switch(state.posts) {
| Init => <div>(ReasonReact.string("Init"))</div>
| Loading => <div>(ReasonReact.string("Loading"))</div>
| Live(posts) => <PostsViewComponent posts />
}
}
}
}

Handing Errors:
If you observe the above codebase carefully, you will notice we implicitly made the assumption that our fetch post API never fails. But we both know that it’s a risk too big for any application that wishes to operate in real world environments.
The API failure can happen due to multiple reasons but for simplicity we handle the various Error types (like Not Authenticated, Not Authorized Internal Server Error etc ) under a single umbrella of Error. Lets see how easy/difficult it is to add support for handling errors in the reason-react application we just created.

Let’s start with the variant definition for type Loadable. We add a new type Error as shown below :-

module Loadable = {  type loadable('t) =    |  Init    |  Loading    |  Live('t)    |  Error
}

Now we compile our reason-react application. The reason compiler will throw the below warning:-

Warning number 8
/Users/prateek/Dev/myGit/loader-component/src/PostsComponent.re 58:26-64:5

56 │ }
57 │ },
58 │ render: ({state}) => {
59 │ switch(state.posts) {
. │ ...
63 │ }
64 │ }
65 │ }
66 │ }

You forgot to handle a possible value here, for example:
Error

This implies that we have not defined what to render when state holds the value of type Loadable.Error. We add a use-case for the same in render as shown below:-

render: ({state}) => {
switch(state.posts) {
| Init => <div>(ReasonReact.string("Init"))</div>
| Loading => <div>(ReasonReact.string("Loading"))</div>
| Live(posts) => <PostsViewComponent posts />
| Error => <div>(ReasonReact.string("Error..."))</div>
}
}

Now, we need an action and reducer pair for handling the state transition to Loadable.Error. Let’s add a new action as shown below

type action =
| LoadData
| OnLoadedData(Posts.posts)
| OnError

Now, again as soon as we do this and compile, the reason compiler comes up with the below warning

Warning number 8
/Users/prateek/Dev/myGit/loader-component/src/PostsComponent.re 44:34-58:5

42 ┆ self.send(LoadData)
43 ┆ },
44 ┆ reducer: (action, _state) => {
45 ┆ switch(action) {
. ┆ ...
57 ┆ }
58 ┆ },
59 ┆ render: ({state}) => {
60 ┆ switch(state.posts) {

It informs us that though we have defined an action use-case for Error, the corresponding reducer is missing. So even if we forget to define the reducer for the new state transition, the compiler will not forget. Nice right ?

Lets add the code snippet for the reducer as shown below

| LoadData => ReasonReact.UpdateWithSideEffects(
{posts: Loadable.Loading},
self =>
Js.Promise.(
Network.fetchPosts()
|> then_(result => resolve(self.send(OnLoadedData(result))))
|> ignore
)
)
| OnLoadedData(data) => ReasonReact.Update(
{posts: Loadable.Live(data)})
| OnError => ReasonReact.Update({posts: Loadable.Error})

Also we need to add the trigger point for the newly defined action OnError. The trigger point should be placed where the control comes after the API fails. So we need to first catch that the error has occurred and then invoke the action OnError. We do this as shown below:-

| LoadData => ReasonReact.UpdateWithSideEffects(
{posts: Loadable.Loading},
self =>
Js.Promise.(
Network.fetchPosts()
|> then_(result => resolve(self.send(OnLoadedData(result))))
|> catch(_error => {
resolve(self.send(OnError))
})

|> ignore
)
)

And we are done. You see, its super easy to add support for an additional use-case. This comes with a 100% guarantee, that the new use case has not introduced any errors into the application courtesy to the static type safety provided by ReasonML.

Summary
ReasonML takes the best of both the React ecosystem and ReasonML world and gives us a powerful library Reason-React to develop scalable and robust web applications.

Extras
Version 0.7.0 of ReasonReact, the latest release of reason-react library, adds support for the React Hooks API. The codebase used in the blogpost does not support this new release. Please find an updated code snippet for this post with React-hooks here. Special thanks to Patrick Stapfer who voluntarily did this migration :-)

In next part we will explore how to refactor the present codebase using abstraction techniques in ReasonML.

A big thanks to the technology team@Porter for their help and support.

Please feel free to put your feedbacks/questions in the comment section.
I am on twitter and insta anytime you need to talk more about ReasonML , Frontend-Development, building products that matter, cooking healthy recipes, fitness, traveling, and anything else that makes sense.

--

--