read

Are you a big fan of React, and want to know more about Reason/OCaml? I made this for you!

Reason is a project that adds a JavaScript-style syntax and a bunch of conventions and tooling to OCaml. The goal is to make this awesome language, with its powerful type system and robust multi-platform compiler, accessible to a broader audience. It's backed by the good folks at Facebook who invented and built React, and so naturally having best-in-class React interop has been a high priority.

This tutorial aims to give you a nice introduction to the syntax and type system of Reason, through the ReasonReact library. We'll be building a simple Todo list application.

What are we building?

We'll build a fairly simple Todo-list application, and work through component state, mutable variables, and responding to click and keyboard events.

Setup

There are a couple of boilerplate-generator type things that you can take advantage of if you want. reason-scripts, create-react-reason-app, or bsb -init will get you started. I show the details here so that you know how it works under the hood.

Clone this starter repo that has all of the config files ready for you. Here's what it contains out of the box:

~$ tree
.
├── bsconfig.json
├── package.json
├── webpack.config.js
├── public
│   ├── index.html
│   └── styles.css
└── src
    ├── Main.re
    └── TodoList.re

bsconfig.json

This tells bucklescript how to compile your code. In it, we specify libraries we depend on (reason-react), that we want to use the new react-jsx transform, and that our files are in src.

{
  "name" : "tic-tac-toe",
  "reason" : {"react-jsx" : 2},
  "bs-dependencies": ["reason-react"],
  "sources": "src"
}

Here's some documentation on the schema of bsconfig.json. Note that source directories are not walked recursively. Subfolders have to be listed out.

package.json

For our development dependencies we have bs-platform (which contains the bucklescript compiler) and webpack (for bundling the compiled js files together).

Our runtime dependencies include both reason-react and the npm libraries that reason-react code depends on, react, and react-dom.

{
  "name": "reason-to-do",
  "scripts": {
    "start": "bsb -make-world -w",
    "build": "webpack -w",
    "clean": "bsb -clean-world"
  },
  "dependencies": {
    "react": "^15.4.2",
    "react-dom": "^15.4.2",
    "reason-react": "0.2.1"
  },
  "devDependencies": {
    "bs-platform": "^1.7.5",
    "webpack": "^3.0.0"
  }
}

npm start will start the bucklescript compiler in watch mode, and npm run build will start our webpack bundler in watch mode. While developing, we'll have both these processes running.

webpack.config.js

Webpack also needs some configuration, so it knows what to compile and where to put it. Notice that bucklescript puts our compiled javascript into ./lib/js/, with parallel file structure to our ./src directory.

module.exports = {
  entry: './lib/js/src/main.js',
  output: {
    path: __dirname + '/public',
    filename: 'bundle.js',
  },
};

Building

Open two terminals, and npm install && npm start in one, npm run build in the other. The one with npm start is bucklescript, which you'll want to keep an eye on -- that's the one that's going to show you error messages if you e.g. have a type error in your code. The webpack one you can pretty much ignore.

Now open public/index.html in your favorite browser, and you should see this!

Step 0: The included code

We've got two reason source files at the moment: Main.re and TodoApp.re.

Main.re
ReactDOMRe.renderToElementWithId <TodoApp /> "root";

Here we have a single function call, which translates (roughly) to ReactDOM.render(<TodoApp />, document.getElementById("root")).

Inter-file dependencies

One thing you'll notice is that there's no require() or import statement indicating where TodoApp came from. In OCaml, inter-file (and indeed inter-package) dependencies are all inferred from your code. Basically, the compiler sees TodoApp isn't defined anywhere in this file, so there must be a file TodoApp.re (or .ml) somewhere that this depends on.

Currently, there is no distinction made between files in your own project and files in libraries you depend on -- meaning that if ReasonReact had a file called Utils.re inside of it, you wouldn't be able to have a file named Utils.re in your project. As you might imagine, this is something of a mess, and is being actively worked on.

ReasonReact's JSX

Let's look at what <TodoApp /> desugars to in ReasonReact:

TodoApp.make [||];

This means "call the make function in the TodoApp module with a single argument, an empty array".

If there had been some props and some children, it would desugar like this:

<TodoApp some="thing" other=12>child1 child2</TodoApp>
/* becomes */
TodoApp.make some::"thing" other::12 [|child1, child2|];

Some key points here

  • Calling a function in Reason, like OCaml and Haskell, doesn't involve parenthesis or commas. a b c is akin to JavaScript's a(b, c). There's an open pull request to move to more js-like syntax.

  • [| val, val |] is array literal syntax. An array is fixed-length & mutable, with O(1) random access, in comparison to a List, which is singly linked & immutable, with O(n) random access.

  • prop values don't have the {} curly wrappers what we know from JSX, they are parsed as expressions. So a=some_vbl_name is perfectly fine.

  • Children are also expressions -- in contrast to JSX, where they are strings by default.

So we know that TodoApp needs to have a make function that takes an array of children. Let's take a look at it.

Defining a component

Mouse over any identifier or expression to see the type that OCaml has inferred for it. The /* ... */ lines are collapsed - click to expand/collapse them.

TodoApp.re
let component = ReasonReact.statelessComponent "TodoApp";

let make children => {
...component,
render: fun self => {
<div className="app">
<div className="title">
(ReasonReact.stringToElement "What to do")
</div>
<div className="items">
(ReasonReact.stringToElement "Nothing")
</div>
</div>
}
};

In the make function, we're taking a children argument (but ignoring it), and returning a component definition. ReasonReact.statelessComponent returns a default component definition (as a record), with various lifecycle methods & other properties that you can override with the ...record spread syntax, which is similar to es6 object spread. In this case, we only want to override the render function.

In Reason, like OCaml, Haskell, and Lisps in general, there is no explicit return statement for designating the result of a function. The value of any block is equal to the value of the last expression in it. In fact, a block is nothing more than a sequence of expressions where we ignore all but the last value.

Our render function takes a single argument, self (here's the type definition). For stateful components, you can access the state via self.state and updated it via self.update. As we're currently stateless, we don't use it at all.

We return some virtual dom elements! Tags that start with lower-case letters (like div) are intepreted as DOM elements, and become straight React.createElement calls in the compiled JS.

ReasonReact.stringToElement is required to satisfy the type system -- we can't drop in React elements and strings into the same array, we have to wrap the strings with this function first. In my code I often have an alias at the top of the file let se = ReasonReact.stringToElement; to make it less cumbersome.

Step 1: Adding some state

Declaring types

Our state will be just a list of Todo items.

TodoApp_1_1.re
type item = {
title: string,
completed: bool,
};
type state = {
/* this is a type w/ a type argument,
* similar to List<Item> in TypeScript,
* Flow, or Java */
items: list item,
};

If you're familiar with flow or typescript this syntax shouldn't look too foreign to you.

One important difference is that you can't nest type declarations like you can in flow or typescript. For example, this is illegal:

type state = {
/* won't compile! */
items: list {
title: string,
completed: bool,
}
}

Another important thing to note is that type names (and indeed variable names) must start with a lower-case letter. Variant (enum) cases and Module names must start with an upper-case letter.

Making a stateful component

We'll start out by changing ReasonReact.statelessComponent to ReasonReact.statefulComponent. Then our make function gets a little more interesting.

TodoApp_1_1.re
let component = ReasonReact.statefulComponent "TodoApp";

/* I've gone ahead and made a shortened name for converting strings to elements */
let se = ReasonReact.stringToElement;
let make children => {
...component,
initialState: fun () => {
items: [{
title: "Write some things to do",
completed: false,
}]
},
render: fun {state: {items}} => {
let numItems = List.length items;
<div className="app">
<div className="title">
(se "What to do")
</div>
<div className="items">
(se "Nothing")
</div>
<div className="footer">
(se ((string_of_int numItems) ^ " items"))
</div>
</div>
}
};

initialState is what you'd expect, and now the first argument to our render function gets useful. The argument destructuring syntax is just like in JavaScript, where we get the state.items right out of the self argument.

I'll leave it as an exercise for the reader to fix it so that it says "1 item" instead of "1 items".

Reacting to events and changing state

Let's make a button that adds an item to the list.

TodoApp_1_2.re
let newItem () => {title: "Click a button", completed: true};
render: fun {state: {items}} => {
let numItems = List.length items;
<div className="app">
<div className="title">
(se "What to do")
<button
onClick=(fun evt => Js.log "didn't add something")
>
(se "Add something")
</button>
</div>
</div>
}
};

If this were JavaScript & React, this is where we'd call this.setState. In ReasonReact, we make an updater function that takes the current state and returns a new one. update's type looks like ('payload => self => update) => ('payload => unit), meaning accepts an updaters function of two arguments, some payload (in our case a click event) and the self, and returns a simple callback that onClick is expecting.

TodoApp_1_3.re
<button
onClick=(update (fun evt {state} => {
ReasonReact.Update {
...state,
items: [newItem(), ...state.items]
}
}))
>
(se "Add something")
</button>

If we determined that we didn't want to update the state (and avoid a re-render), we could return ReasonReact.NoUpdate.

Update and NoUpdate are examples of variant values in Reason (kinda like enums but much better), which you will be familiar if you've used Swift or Haskell. In TypeScript and Flow, we approximate these with tagged unions.

Now when we click the button, the count at the bottom goes up!

Step 2: Rendering items

The TodoItem component

We're going to want to have a component for rendering the items, so let's make one. Since it's small, we won't have it be its own file -- we'll use a nested module.

TodoApp_2_1.re
type item = {
title: string,
completed: bool,
};

let se = ReasonReact.stringToElement;

let module TodoItem = {
let component = ReasonReact.statelessComponent "TodoItem";
let make ::item children => {
...component,
render: fun self =>
<div className="item">
<input
_type="checkbox"
checked=(Js.Boolean.to_js_boolean item.completed)
/* TODO make interactive */
/>
(se item.title)
</div>
};
};

So this is another stateless component, except this one accepts a property: item. The ::argname syntax means "this function takes a labeled argument which is known as item both externally and internally". Swift and Objective C also allow you have labeled arguments, with an external name that is optionally different from the internal one. If you wanted them to be different, you would write e.g. fun externalFacingName::internalFacingName =>. children is an unnamed argument.

In Reason, named arguments can be given in any order, but unnamed arguments cannot. So if you had a function let myfn = fun ::a ::b c d => {} where c was an int and d was a string, you could call it myfn b::2 a::1 3 "hi" or myfn a::3 3 "hi" b::1 but not myfn a::2 b::3 "hi" 4.

Rendering a list

Now that we've got a TodoItem component, let's use it! We'll replace the section that's currently just (se "Nothing") with this:

TodoApp_2_1.re
<div className="items">
(ReasonReact.arrayToElement
(Array.of_list
(List.map (fun item => <TodoItem item />) items)
)
)
</div>

In the center of all this you can see the function that takes our data and renders a react element.

fun item => <TodoItem item />

Another difference from JSX is that an attribute without a value is "punned", meaning that <TodoItem item /> is the same as <TodoItem item=item />. In JSX, lone attributes are interpreted as <TodoItem item={true} />.

ReasonReact.arrayToElement (Array.of_list (List.map /*...*/ items))

And now we've got the nuts and bolts of calling that function for each item and appeasing the type system. Another way to write the above is

List.map /*...*/ items |> Array.of_list |> React.arrayToElement

The pipe |> is a left-associative binary operator that's defined as a |> b == b a. It can be quite nice when you've got some data and you just need to pipe it through a list of conversions.

Tracking ids w/ a mutable ref

If you're familiar with React, you'll know that we really ought to be using a key to uniquely identify each rendered TodoItem, and in fact we'll want unique keys once we get around to modifying the items as well.

Let's add an id property to our item type, and add an id of 0 to our initialState item.

TodoApp_2_2.re
type item = {
id: int,
title: string,
completed: bool,
};
initialState: fun () => {
items: [{
id: 0,
title: "Write some things to do",
completed: false,
}]
},

But then, what do we do for the newItem function? We want to make sure that each item created has a unique id, and one way to do this is just have a variable that we increment by one each time we create a new item.

TodoApp_2_2.re
let lastId = 0;
let newItem () => {
let lastId = lastId + 1;
{id: lastId, title: "Click a button", completed: true};
};

Of course this won't work -- we're just defining a new variable that's only scoped to the newItem function. At the top level, lastId remains 0. In order to simulate a mutable let binding, we'll use a ref.

TodoApp_2_3.re
let lastId = ref 0;
let newItem () => {
lastId := !lastId + 1;
{id: !lastId, title: "Click a button", completed: true};
};

You update a ref with :=, and to access the value you dereference it with !. Now we can add our key property to the <TodoItem> components.

TodoApp_2_3.re
(List.map (fun item => <TodoItem
key=(string_of_int item.id)
item
/>) items)

Step 3: Full interactivity

Checking off items

Now that our items are uniquely identified, we can enable toggling. We'll start by adding an onToggle prop to the TodoItem component, and calling it when the div gets clicked.

TodoApp_3_1.re
let module TodoItem = {
let component = ReasonReact.statelessComponent "TodoItem";
let make ::item ::onToggle children => {
...component,
render: fun _ =>
<div className="item" onClick=(fun evt => onToggle())>