In this tutorial, we're going to build a small weather app using Reason. There's a link to the source code at the bottom of the page. This tutorial assumes a basic understanding of React, as we'll be using the ReasonReact bindings to build the app. If you haven't used React before, this article is a great place to start.
What is Reason?
Reason is a new syntax for OCaml, developed by Facebook with a heavy influence from JavaScript. It’s got 100% type coverage, which has resulted in a very powerful type system.
Reason is also suitable for cross-platform development. We can use BuckleScript to compile our code into (readable) JavaScript, which opens up the entire web platform. Thanks to OCaml, it's also possible to use Reason for native development.
In addition, Reason can access the entire JS and OCaml ecosystems, and offers ReasonReact for building UI components with ReactJS. There’s a useful page in the docs which explains the advantages in more detail!
Requirements
First of all, let's make sure we've got the right tools installed.
We'll be using Create React App to bootstrap the project. If you haven't used it before, install by running npm i -g create-react-app
. There are two other packages we need to get started:
Reason CLI: the Reason toolchain. Check the installation docs.
At the time of writing, macOS users can install by runningnpm i -g reason-cli@3.1.0-darwin
BuckleScript:
npm i -g bs-platform
I'm also using the vscode-reasonml editor plugin. If you're using a different editor, check the list of plugins to find the right one for you.
Our first component
To get started, we'll create the boilerplate code for our app:
create-react-app weather-app --scripts-version reason-scripts
This gives us a basic App component:
We can compile & run this using yarn start
. Let's take a look at a few interesting parts...
[%bs.raw {|require('./app.css')|}];
BuckleScript allows us to mix raw JavaScript into our Reason code, from a one-liner to an entire library (if we're just hacking around). This should be used sparingly, but can be a useful escape hatch whilst we're getting started.
let component = ReasonReact.statelessComponent("App");
We'll be using two types of ReasonReact component: statelessComponent
and reducerComponent
. Stateless components do what they say on the tin. Reducer components are stateful, and have Redux-like reducers built-in. We'll come onto these later.
let make = (~message, _children) => { ... }
This is the method that defines our component. The two parameters have different symbols: ~
is a labelled argument, meaning we can reference the parameter by name, and _
is a more explicit way of showing that the parameter isn't used (the compiler will give us a warning otherwise).
The ...component
spread operator means that our make
function is building upon the component we just defined, overwriting the defaults.
<h2> (ReasonReact.stringToElement(message)) </h2>
JSX in Reason is more strict than in normal React. Instead of just writing <h2>{message}</h2>
, we have to explicitly convert the message
string to a JSX element.
We'll be using this boilerplate when we build our own components later on.
Types in Reason
Let's create a new file, WeatherData.re
. This will define the data structure and any related methods for our Weather record. To begin with, let's create the type:
Within this file, we can create new records using this data structure, and the compiler will know that it's a Weather item. From other files, we'll need to tell the compiler what the type is. In Reason, files can be referenced as modules, meaning we don't have to explicitly import them! We can just do this:
I mentioned earlier that Reason has 100% type coverage, but we've only defined our Weather type... where does the rest of the coverage come from? We could explicitly define a type for every variable we use, e.g. let greeting: string = "Hello";
but fortunately the OCaml system can infer types for us. So if we write let greeting = "Hello";
the compiler will still know that greeting
is a string. This is a key concept in Reason and guarantees type safety.
Keeping state
Moving back to our project, let's modify app.re
so it can store the data we want to display. This will involve:
- Defining the type of our state
- Setting our initial state (with some dummy data, for now)
- Defining actions that can be applied to state
- Defining reducers for the component to handle these
Actions define the different things we can do to manipulate state. For example, Add
or Subtract
. Reducers are pure functions which define how state should be affected by these actions, just like in Redux. They take the action and our previous state as parameters, and return an update type.
There are two new Reason concepts here: variants and pattern matching.
type action =
| WeatherLoaded(WeatherData.weather);
This is a variant: a data structure which represents a choice of different values (like enums). Each case in a variant must be capitalised, and can optionally receive parameters. In ReasonReact, actions are represented as variants. These can be used with the switch
expression:
switch action {
| WeatherLoaded(newWeather) =>
ReasonReact.Update({ ... })
}
This is one of the most useful features in Reason. Here we're pattern matching action
, based on the parameter we receive in the reducer()
method. The compiler knows that our switch statement needs to handle every case of action
. If we forget to handle a case, the compiler knows, and will tell us!
We used destructuring to access the value of newWeather
in a previous example. We can also use this to match actions based on the values they contain. This gives us some very powerful behaviour!
Fetching data
So far, our app renders the dummy weather data - now let's load it from an API. We'll put the methods for fetching and parsing data in our existing WeatherData.re
file.
Firstly, we need to install bs-fetch: npm i bs-fetch
and bs-json: npm i @glennsl/bs-json
. We also need to add them to our bsconfig.json
:
{
...
"bs-dependencies": [
"bs-fetch"
"@glennsl/bs-json"
]
}
We'll be using the Yahoo Weather API to fetch our data. Our getWeather()
method will call the API, then parse the result using parseWeatherResultsJson()
, before resolving with a weather
item:
Json.parseOrRaise(json) |> Json.Decode.(at([
...
], parseWeatherJson));
This parses the JSON string response, before traversing the data via the specified fields. It then uses the parseWeatherJson()
method to parse the data found inside the condition
field.
Json.Decode.{
summary: field("text", string, json),
temp: float_of_string(field("temp", string, json))
};
In this snippet, field
and string
are properties of Json.Decode
. This new syntax "opens" Json.Decode
, so its properties can be used freely within the curly brackets (instead of repeating Json.Decode.foo
). The code generates a weather
item, using the text
and temp
fields to assign summary
and temp
values.
float_of_string
does exactly what you'd expect: it converts the temperature from a string (as we get from the API) into a float.
Updating state
Now we've got a getWeather()
method which returns a promise, we need to call this when our App component loads. ReasonReact has a similar set of lifecycle methods to React.js, with a few small differences. We'll be using the didMount
lifecycle method for making the API call to fetch the weather.
First of all, we need to change our state to show that it's possible to not have a weather item in state - we'll get rid of the dummy data. option()
is a built-in variant in Reason, which describes a "nullable" value:
type option('a) = None | Some('a);
We need to specify None
in our state type and initial state, and Some(weather)
in our WeatherLoaded
reducer:
Now we can actually make the API request when our component mounts. Looking at the code below, handleWeatherLoaded
is a method which dispatches our WeatherLoaded
action to the reducer. When the promise resolves, it will be handled by our reducer, and the state will be updated!
Note: it's important to return ReasonReact.NoUpdate
from most component lifecycles. The reducer will handle all state changes at the next opportunity.
If we run our app now, we'll run into an error... We're currently trying to render information about self.state.weather
, but this is set to None
until we receive a response from the API. Let's update our App component to show a loading message while we wait:
And the result...
Error handling
One thing we haven't thought about is what happens if we can't load our data. What if the API is down, or it returns something we're not expecting? We'll need to recognise this and reject the promise:
switch (parseWeatherResultsJson(jsonText)) {
| exception e => reject(e);
| weather => resolve(weather);
};
This switch statement tries to parse the API response. If an exception is raised, it will reject the promise with that error. If the parsing was successful, the promise will be resolved with the weather item.
Next, we'll change our state to let us recognise if an error has occurred. Let's create a new type which adds an Error
case to our previous Some('a)
or None
.
Whilst doing this, we'll also need to add an Error
case to our render function - I'll let you add that yourself. Finally, we need to create a new action and reducer to be used when our getWeather()
promise rejects.
These are concepts we've used already, but it's useful to let the user know if something goes wrong. We don't want to leave them hanging with a "loading" message!
There we have it, our first ReasonReact web app. Nice work! We've covered a lot of new concepts, but hopefully you can already see some of the benefits of using Reason.
If you found this interesting & would like to see another post building upon this, please let me know by clicking a reaction below! ❤️ 🦄 🔖
Further reading
A little more context, including a link to the source code.
Exploring ReasonML and functional programming - a free online book about (you guessed it) Reason and FP.
OSS projects
- bs-jest - BuckleScript bindings for Jest.
- lwt-node - a Reason implementation of the Node.js API
- reason-apollo - bindings for Apollo client and React Apollo
Other
- Discord channel
- Forum
- Reason Town - a podcast on the ReasonML language and community
- Redex - the Reason package index
Top comments (1)
Fantastic, very nice explanation of a promising new technology, this article deservers more likes!