A ReasonReact Tutorial

July
5,
2017
ยท
react,
tutorial,
ocaml,
reason

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

This tutorial was updated on April 20, 2019 for reason-react version 0.7.0, and React hooks! If you want to see what it was like before hooks, here's the previous version

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" : "a-reason-react-tutorial",
  "reason" : {"react-jsx" : 3},
  "refmt": 3,
  "bs-dependencies": ["reason-react"],
  "sources": "src"
}

Here's some documentation on the schema of bsconfig.json. Note that source directories are not walked recursively by default. You can specify them explicitly, or use "subdirs": true.

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": "bsb -make-world",
    "bundle": "webpack",
    "clean": "bsb -clean-world"
  },
  "dependencies": {
    "react": "16.8.6",
    "react-dom": "16.8.6",
    "reason-react": "0.7.0"
  },
  "devDependencies": {
    "bs-platform": "5.0.3",
    "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.

If you're using VSCode and my reason-vscode extension, you can skip the npm start bit -- the extension will run bucklescript for you & report errors inline.

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
1 ReactDOMRe.renderToElementWithId(<TodoApp title="What to do" />, "root");

Here we have a single function call, which translates (roughly) to ReactDOM.render(<TodoApp hello="world" />, 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.

ReasonReact's JSX

Let's look at what <TodoApp title="What to do" /> desugars to in ReasonReact:

1 React.createElement(TodoApp.make, TodoApp.makeProps(~title="What to do", ()));

So ReasonReact expects there to be a make function that's a React function component, and a makeProps function as well.

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

1 <TodoApp some="thing" other=12> child1 child2 </TodoApp>;
2 /* becomes */
3 React.createElement(TodoApp.make, TodoApp.makeProps(~some="thing", ~other=12, ~children=[|child1, child2|], ()));

Some key points here

  • [| 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 that 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 and a makeProps function. 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
1 [@react.component]
2 let make = (~title) => {
3 <div className="app">
4 <div className="title">
5 (React.string(title))
6 </div>
7 <div className="items">
8 (React.string("Nothing"))
9 </div>
10 </div>
11 };

For our TodoApp component, the make function acts like very similar to a "function component" in ReactJS, where it takes props as arguments, only in Reason we used named function arguments instead of a props object. Then the [@react.component] decorator automacigally creates a makeProps function that will take those labelled arguments and turn them into a normal JavaScript props object that React can understand. It also modifies the make function so that it consumes that JavaScript object from React.

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.

And we return some virtual dom elements! Tags that start with lower-case letters (like div) are intepreted as DOM elements, just like in JavaScript.

React.string 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 str = React.string; 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
1 type item = {
2 title: string,
3 completed: bool
4 };
5
6 type state = {
7 /* this is a type w/ a type argument,
8 * similar to List<Item> in TypeScript,
9 * Flow, or Java */
10 items: list(item)
11 };

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:

1 type state = {
2 /* won't compile! */
3 items: list({
4 title: string,
5 completed: bool,
6 })
7 }

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

Adding some state with useReducer

With React's hooks, going from stateless to stateful is as easy as dropping in a function call.

TodoApp_1_1.re
12
13 // I've gone ahead and made a shortened name for converting strings to elements
14 let str = React.string;
15
16 [@react.component]
17 let make = () => {
18 let ({items}, dispatch) = React.useReducer((state, action) => {
19 // We don't have state transitions yet
20 state
21 }, {
22 items: [
23 {title: "Write some things to do", completed: false}
24 ]
25 });
26 let numItems = List.length(items);
27 <div className="app">
28 <div className="title"> (str("What to do")) </div>
29 <div className="items"> (str("Nothing")) </div>
30 <div className="footer">
31 (str(string_of_int(numItems) ++ " items"))
32 </div>
33 </div>
34 };

useReducer acts the same as in ReactJS, taking a reducer function and an initial state, and returning the current state and a dispatch function. Here we're destructuring the current state in the let binding, similar to JavaScript, so we have access to items immediately.

The reducer function is currently a no-op, because we don't yet update the state at all. It will get more interesting later.

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
9 let newItem = () => {title: "Click a button", completed: true};
22 let numItems = List.length(items);
23 <div className="app">
24 <div className="title">
25 (str("What to do"))
26 <button
27 onClick=((_evt) => Js.log("didn't add something"))
28 >
29 (str("Add something"))
30 </button>
31 </div>
36 </div>
37 };

If this were classes-style JavaScript & React, this is where we'd call this.setState. In new-style React.js, with useReducer, we'd call dispatch, probably with a string or javascript object, and handle it there. In ReasonReact, we'll first make an action type, which describes the ways that our state can be updated. To start there will be only one way to update it; adding a pre-defined item. We then make our reducer function handle that action type.

TodoApp_1_3.re
8 type action =
9 | AddItem;
17 let ({items}, dispatch) = React.useReducer((state, action) => {
18 switch action {
19 | AddItem => {items: [newItem(), ...state.items]}
20 }
21 }, {

Then we can change the onClick handler to trigger that action. We do so by calling dispatch with an action as the argument.

TodoApp_1_3.re
31 <button onClick=((_evt) => dispatch(AddItem))>
32 (str("Add something"))
33 </button>

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
1 type item = {
2 title: string,
3 completed: bool
4 };
5
6 let str = React.string;
7
8 module TodoItem = {
9 [@react.component]
10 let make = (~item) => {
11 <div className="item">
12 <input
13 type_="checkbox"
14 checked=(item.completed)
15 /* TODO make interactive */
16 />
17 (str(item.title))
18 </div>
19 };
20 };

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. (~externalFacingName as internalFacingName) =>.

In Reason, named arguments can be given in any order, but unnamed arguments cannot. So if you had a function let myfn = (~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 str("Nothing") with this:

TodoApp_2_1.re
49 <div className="items">
50 (
51 React.array(Array.of_list(
52 List.map((item) => <TodoItem item />, items)
53 ))
54 )
55 </div>

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

1 (item) => <TodoItem item />;

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

1 React.array(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

1 List.map(/*...*/, items) |> Array.of_list |> React.array

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
1 type item = {
2 id: int,
3 title: string,
4 completed: bool
5 };
40 }, {
41 items: [{
42 id: 0,
43 title: "Write some things to do",
44 completed: false
45 }]
46 });

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
28 let lastId = 0;
29 let newItem = () => {
30 let lastId = lastId + 1;
31 {id: lastId, title: "Click a button", completed: true}
32 };

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
29 let lastId = ref(0);
30 let newItem = () => {
31 lastId := lastId^ + 1;
32 {id: lastId^, title: "Click a button", completed: true}
33 };

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
59 (item) => <TodoItem
60 key=(string_of_int(item.id))
61 item
62 />, 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
9 module TodoItem = {
10 [@react.component]
11 let make = (~item, ~onToggle) => {
12 <div className="item" onClick=((_evt) => onToggle())>

So onToggle has the type unit => unit. We now need to define another action, and the way to handle it. And then we pass the action creator to onToggle.

TodoApp_3_1.re
26 type action =
27 | AddItem
28 | ToggleItem(int);
39 switch action {
40 | AddItem => {items: [newItem(), ...state.items]}
41 | ToggleItem(id) =>
42 let items = List.map(
43 (item) =>
44 item.id === id
45 ? {...item, completed: !item.completed}
46 : item,
47 state.items
48 );
49 {items: items}
50 }
69 (item) => <TodoItem
70 key=(string_of_int(item.id))
71 onToggle=(() => dispatch(ToggleItem(item.id)))
72 item
73 />, items

Let's look a little closer at the way we're handling ToggleItem:

TodoApp_3_1.re
41 | ToggleItem(id) =>
42 let items = List.map(
43 (item) =>
44 item.id === id
45 ? {...item, completed: !item.completed}
46 : item,
47 state.items
48 );
49 {items: items}

We map over the list of items, and when we find the item to toggle we flip the completed boolean.

Text input

Having a button that always adds the same item isn't the most useful -- let's replace it with a text input. For this, we'll make another nested module component.

TodoApp_final.re
25 module Input = {
26 type state = string;
27 [@react.component]
28 let make = (~onSubmit) => {
29 let (text, setText) = React.useReducer((oldText, newText) => newText, "");
30 <input
31 value=text
32 type_="text"
33 placeholder="Write something to do"
34 onChange=((evt) => setText(valueFromEvent(evt)))
35 onKeyDown=((evt) =>
36 if (ReactEvent.Keyboard.key(evt) == "Enter") {
37 onSubmit(text);
38 setText("")
39 }
40 )
41 />
42 };
43 };

For this component, our state is just a string, and we only ever want to replace it with a new string, so our usage of useReducer is a lot simpler, and we can call dispatch setText.

Most of this we've seen before, but the onChange and onKeyDown handlers are new.

TodoApp_final.re
34 onChange=((evt) => setText(valueFromEvent(evt)))
35 onKeyDown=((evt) =>
36 if (ReactEvent.Keyboard.key(evt) == "Enter") {
37 onSubmit(text);
38 setText("")
39 }
40 )

The input's onChange prop is called with a Form event, from which we get the text value and use that as the new state.

TodoApp_final.re
23 let valueFromEvent = (evt): string => evt->ReactEvent.Form.target##value;

In JavaScript, we'd do evt.target.value to get the current text of the input, and this is the ReasonReact equivalent. ReasonReact's bindings don't yet have a well-typed way to get the value of an input element, so we use ReactEvent.Form.target to get the "target element of the event" as a "catch-all javascript object", and get out the value with the "JavaScript accessor syntax" ##value.

This is sacrificing some type safety, and it would be best for ReasonReact to just provide a safe way to get the input text directly, but this is what we have for now. Notice that we've annotated the return value of valueFromEvent to be string. Without this, OCaml would make the return value 'a (because we used the catch-all JavaScript object) meaning it could unify with anything, similar to the any type in Flow.

The -> that we're using here, called Pipe First, is similar to the |> operator, but it passes the "thing on the left" as the first argument of the "thing on the right", instead of the last. It can do this because it's a syntax transform instead of a normal function, and this allows for a more fluid style. With the normal pipe, you'd do (evt |> ReactEvent.Form.target)##value or evt |> ReactEvent.Form.target |> (x => x##value). You can think of -> kind of like method dispatch in go and rust, where a function gets called with the "thing on the left" as the first argument.

TodoApp_final.re
35 onKeyDown=((evt) =>
36 if (ReactEvent.Keyboard.key(evt) == "Enter") {
37 onSubmit(text);
38 setText("")
39 }
40 )

ReasonReact does provide a nice function for getting the key off of a keyboard event. So here we check if they pressed Enter, and if they did we call the onSubmit handler with the current text and then clear the input.

And now we can replace that filler "Add something" button with this text input. We'll change the AddItem action to take a single argument, the text of the new item, and pass that to our newItem function.

TodoApp_final.re
48 type action =
49 | AddItem(string)
50 | ToggleItem(int);
51
52 let lastId = ref(0);
53 let newItem = (text) => {
54 lastId := lastId^ + 1;
55 {id: lastId^, title: text, completed: false}
56 };
61 switch action {
62 | AddItem(text) => {items: [newItem(text), ...state.items]}
82 <div className="title">
83 (str("What to do"))
84 <Input onSubmit=((text) => dispatch(AddItem(text))) />
85 </div>

And that's it!

๐Ÿ˜“ thanks for sticking through, I hope this was helpful! There's definitely more we could do to this Todo list, but hopefully this gave you a good primer on how to navigate Reason and ReasonReact.

If there was anything confusing, let me know on twitter or open an issue on github. If you want to know more, come join our reasonml channel on Discord or the OCaml Discourse forum.

Other posts I've written about Reason: