Create ReactJS App with ReasonML

In this article, I will show, how you can use ReasonML, ReasonML React, Fetch API (bs-fetch package), ReactDOMRe.Style and Material UI CSS library to create the application.

There are not so many different materials on how to create Reason React app, what is labeled arguments, how to import component, how to define a custom style for components and so on. In this article, I will try to answer all of these questions.

Requirements for our application

It will fetch data from https://openweathermap.org to read atmospheric pressures for next 7 days. We will analyze this data to show dates, which have large differences in an atmospheric pressure. Some people are sensitive to such weather changes, so they can use it to plan their week accordingly.

Dates with larger differences will get a richer, redder color. Days without changes will be green:

You can check live app: www.atmoache.me

It is hosted on GitHub, so all sources are here: https://github.com/gladimdim/atmoache/

The application was built from scratch using https://reasonml.github.io/reason-react/docs/ru/installation.html utility to create project structure.

Entry Point of ReasonReact App

Our index.html file looks very standard:

We load CSS from MUI CSS library, then load index.js script.

Index.js is a bundle generated by webpack, it contains React, Reason and all other packages we need for runtime.

Entry point is written in src/index.re file:

ReactDOMRe.renderToElementWithId(<MainApp />, "index");

Here we just mount our component “MainApp” to div with id “index”.

The good thing about ReasonML is that you do not have to explicitly import other files or components. In our example, we told React to render <MainApp> component. ReasonML will look for MainApp.re file and call a function ‘make’ from it:

...
let make = (_) => {
...component,
initialState: () => {cityName: "Kyiv", pressures: [||]},
reducer: (action, state) =>
...

For our React Component, we need to have the state and possible actions.

The state has two properties — city name and an array of pressures:

type state = {
cityName: string,
pressures: tArrayPressures
};

Possible actions are:

type action =
| PressureLoaded(tArrayPressures)
| UpdateCity(string)
| LoadPressure;
  • UpdateCity action is called when user enters new text into text field.
  • LoadPressure is issued by button. It will call REST API and process response.
  • PressureLoaded is called when JSON is processed and component needs to render an array of atmospheric pressure changes.

React Render method

Now let’s check component’s render method:

render: self =>
<div>
<Controls onCitySet=(newCity => self.send(UpdateCity(newCity))) />
<div className="mui-container">
(
calc(self.state.pressures)
|> dropFirst
|> Array.mapi((index, pressure) =>
<PressureItem
key=(string_of_int(index))
pressure
date=(indexToDate(index))
/>
)
|> ReasonReact.arrayToElement
)
</div>
</div>

As you see, it renders div with another React Component <Controls> at the top, then renders <PressureItem> elements one by one. We do not explicitly require or import these new components. Appropriate Controls.re and PressureItem.re files must be present in your ‘src’ directory.

Integration of Reason Components

<Controls> component’s make method is defined like this:

let make = (~onCitySet, _children) => {
...component,
initialState: () => {cityName: "Kyiv"},

~onCitySet is a labeled argument and is required by this component. Underscore in front of ‘_children’ argument means it is not used inside make function. Reason compiler will give you a warning about unused ‘children’ argument but will ignore it, if you add underscore as prefix: ‘_children’:

[2/2] Building src/Controls.mlast.d
[1/3] Building src/Controls-ReactTemplate.cmj
Warning number 27
/Users/dgl/projects/atmoache/src/Controls.re 8:25-32
 6 │ let component = ReasonReact.reducerComponent("Controls");
7 │
8 │ let make = (~onCitySet, children) => {
9 │ ...component,
10 │ initialState: () => {cityName: "Kyiv"},
unused variable children.

You can see, ~onCitySet has no type annotation neither at calling site nor at function definition. ReasonML automatically defined a type for it. Let’s see it in action.

onCitySet is called like this in Controls.re:

<button
className="mui-btn mui-btn--raised mui-btn--danger"
onClick=((_) => onCitySet(self.state.cityName))>
(ReasonReact.stringToElement("Get Pressure Changes"))
</button>

And defined in MainApp like this:

<Controls onCitySet=(newCity => self.send(UpdateCity(newCity))) />

So we defined it as a function which takes one parameter, and when called, it calls Redux action UpdateCity(newCity).

Let’s make a mistake and provide two arguments:

<Controls onCitySet=((newCity, oldCity) => self.send(UpdateCity(newCity)))
/>

and compiler gives us error:

We've found a bug for you!
/Users/dgl/projects/atmoache/src/MainApp.re 114:19-72
 112 ┆ <div>
113 ┆ <Controls
114 ┆ onCitySet=((newCity, oldCity) => self.send(UpdateCity(newCity)))
115 ┆ />
116 ┆ <div className="mui-container">
This function expects too many arguments, it should have type
(string) => unit

Let’s try to trick compiler and call onCitySet with integer argument:

<button
className="mui-btn mui-btn--raised mui-btn--danger"
onClick=((_) => onCitySet(1))>
(ReasonReact.stringToElement("Get Pressure Changes"))
</button>

Compiler gives us error and even tells us how to fix issue:

We've found a bug for you!
/Users/dgl/projects/atmoache/src/MainApp.re 113:60-66
 111 ┆ render: self =>
112 ┆ <div>
113 ┆ <Controls onCitySet=(newCity => self.send(UpdateCity(newCity))) /
>
114 ┆ <div className="mui-container">
115 ┆ (
This has type:
int
But somewhere wanted:
string
You can convert a int to a string with string_of_int.

So you can see, that ReasonML compiler type checks interfaces between different components. We cannot call callbacks with types, which are not suitable for the main component, and we cannot define callbacks which are not suitable for their callers.

Redux in ReasonReact

Redux comes built into ReasonReact. You have to define actions:

type action =
| CityNameChanged(string);

and state:

type state = {cityName: string};

and then define reducer function for component:

reducer: action =>
switch action {
| CityNameChanged(s) => (_state => ReasonReact.Update({cityName: s}))
},

Controls.re component has one possible action which just updates cityName property in state. In this case update of state is synchronous, but what if we can update state only after some async function is finished (calling REST for example)?

Updating State after Async Function is finished executing

For example, in MainApp we have LoadPressure action which calls JSON, then parses its response, decodes it and only then it can update state with an array of atmospheric pressures.

Implementing it like this will not work:

| LoadPressure =>
Js.Promise.(
Fetch.fetch(
"http://api.openweathermap.org/data/2.5/forecast/daily?q="
++ state.cityName
++ "&cnt=7&mode=json&appid=:)"
)
|> then_(Fetch.Response.json)
|> then_(json =>
json
|> Decode.pressures
|> (
pressures => {
self.send(PressureLoaded(pressures)) |> ignore;
resolve(pressures);
}
)
)
|> ignore
)

)

self.send(PressureLoaded(pressure)) will be executed but defined Redux action will be not called.

To modify state in the async function we must use another method: ReasonReact.SideEffects. So we should wrap our Js.Promise call in this function:

| LoadPressure =>
ReasonReact.SideEffects(
(
self =>
Js.Promise.(
Fetch.fetch(
"http://api.openweathermap.org/data/2.5/forecast/daily?q="
++ state.cityName
++ "&cnt=7&mode=json&appid=:)"
)
|> then_(Fetch.Response.json)
|> then_(json =>
json
|> Decode.pressures
|> (
pressures => {
self.send(PressureLoaded(pressures)) |> ignore;
resolve(pressures);
}
)
)
|> ignore
)
)
)

Updating State before and after another async function is called

In our MainApp component we have such case: button “Get Pressures” is pressed, we update state.cityName with new string, then call LoadPressure Redux Action which also updates state.

This can be achieved by ReasonReact.UpdateWithSideEffects function:

switch action {
| UpdateCity(s) =>
ReasonReact.UpdateWithSideEffects(
{...state, cityName: s}, // update state to this
(self => self.send(LoadPressure)) // then call other Redux action
)

So when UpdateCity action is called, we update state.cityName and then call LoadPressure action.

Type Safety for HTML

One of the nice perks, when using ReasonReact is that it type checks HTML.

For example, <input> element has “placeholder” attribute in Standard, but if you mistype it as “placeHolder”, you will get a compilation error:

Adding Styles to Application

Styles can be added like in any other normal application via classes:

<button
className="mui-btn mui-btn--raised mui-btn--danger"
onClick=((_) => onCitySet(self.state.cityName))>
(ReasonReact.stringToElement("Get Pressure Changes"))
</button>

Style attribute in ReasonReact is also type checked…

In ReasonReact you cannot just write:

<div style="background-color: red;"/>

It gives you this error:

We've found a bug for you!
/Users/dgl/projects/atmoache/src/PressureItem.re 29:37-60
 27 ┆ ...component,
28 ┆ render: _self =>
29 ┆ <div className="div-diff" style="background-color: red;">
30 ┆ (ReasonReact.stringToElement(date))
31 ┆ </div>
This has type:
string
But somewhere wanted:
ReactDOMRe.style

You have to use ReactDOMRe.Style.make, so ReasonReact type checks possible keys and values in your styles. In PressureItem.re I use helper function which translates pressure difference from float value into background colors in RGB format.

let calculateStyle = (pressure: float) : ReactDOMRe.style =>
ReactDOMRe.Style.make(~backgroundColor=calculateDiff(pressure)
, ());
let make = (~pressure: float, ~date: string, _children) => {
...component,
render: _self =>
<div
className="div-diff" style=(calculateStyle(Js_math.abs_float(pressure)))>
(ReasonReact.stringToElement(date))
</div>
};

You can reference https://github.com/reasonml/reason-react/blob/master/src/ReactDOMRe.re for all possible labeled arguments.

The End

I hope, that this post helped you to learn something new about ReasonML and Reason React.

Materials used when rewriting www.atmoache.me with ReasonML:

Related posts

Leave a Reply

Be the First to Comment!

Notify of
avatar
wpDiscuz