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
.
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:
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:
2 /* becomes */
3 ReactcreateElement(TodoAppmake TodoAppmakeProps(~some"thing" ~other12 ~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. Soa=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.
2 let make = (~title) {
3 <div className="app">
4 <div className="title">
5 (Reactstring(title))
6 </div>
7 <div className="items">
8 (Reactstring("Nothing"))
9 </div>
10 </div>
11 }
13 let hidden = "cool"
14 /* in future snippets there will be real code hidden */
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.
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 }
13 // I've gone ahead and made a shortened name for converting strings to elements
14 let str = Reactstring
15
16 [@react.component]
17 let make = () {
18 let ({items} dispatch) = ReactuseReducer((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 = Listlength(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 }
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:
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.
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 }
13 // I've gone ahead and made a shortened name for converting strings to elements
14 let str = Reactstring
15
16 [@react.component]
17 let make = () {
18 let ({items} dispatch) = ReactuseReducer((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 = Listlength(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.
2 title string
3 completed bool
4 }
5 type state = {
6 items list(item)
7 }
8
11 let str = Reactstring
12 [@react.component]
13 let make = () {
14 let ({items} dispatch) = ReactuseReducer((state action) {
15 state
16 } {
17 items [{
18 title "Write some things to do"
19 completed false
20 }]
21 })
23 <div className="app">
24 <div className="title">
25 (str("What to do"))
26 <button
27 onClick=((_evt) Jslog("didn't add something"))
28 >
29 (str("Add something"))
30 </button>
31 </div>
33 <div className="footer">
34 (str(string_of_int(numItems) " items"))
35 </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.
2 title string
3 completed bool
4 }
5
6 type state = {items list(item)}
7
9 AddItem
11 let newItem = () {title "Click a button" completed true}
12
13 let str = Reactstring
14
15 [@react.component]
16 let make = () {
18 switch action {
19 AddItem => {items [newItem() ...stateitems]}
20 }
21 } {
23 title "Write some things to do"
24 completed false
25 }]
26 })
27 let numItems = Listlength(items)
28 <div className="app">
29 <div className="title">
30 (str("What to do"))
31 <button onClick=((_evt) dispatch(AddItem))>
32 (str("Add something"))
33 </button>
34 </div>
35 <div className="items"> (str("Nothing")) </div>
36 <div className="footer">
37 (str(string_of_int(numItems) " items"))
38 </div>
39 </div>
40 }
Then we can change the onClick
handler to trigger that action. We do so by calling dispatch
with an action as the argument.
2 title string
3 completed bool
4 }
5
6 type state = {items list(item)}
7
8 type action =
9 AddItem
10
11 let newItem = () {title "Click a button" completed true}
12
13 let str = Reactstring
14
15 [@react.component]
16 let make = () {
17 let ({items} dispatch) = ReactuseReducer((state action) {
18 switch action {
19 AddItem => {items [newItem() ...stateitems]}
20 }
21 } {
22 items [{
23 title "Write some things to do"
24 completed false
25 }]
26 })
27 let numItems = Listlength(items)
28 <div className="app">
29 <div className="title">
30 (str("What to do"))
32 (str("Add something"))
33 </button>
35 <div className="items"> (str("Nothing")) </div>
36 <div className="footer">
37 (str(string_of_int(numItems) " items"))
38 </div>
39 </div>
40 }
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.
2 title string
3 completed bool
4 }
5
6 let str = Reactstring
7
8 TodoItem = {
9 [@react.component]
10 let make = (~item) {
11 <div className="item">
12 <input
13 type_="checkbox"
14 checked=(itemcompleted)
15 /* TODO make interactive */
16 />
17 (str(itemtitle))
18 </div>
19 }
20 }
22 type state = {items list(item)}
23
24 type action =
25 AddItem
26
27 let newItem = () {title "Click a button" completed true}
28
29 [@react.component]
30 let make = () {
31 let ({items} dispatch) = ReactuseReducer((state action) {
32 switch action {
33 AddItem => {items [newItem() ...stateitems]}
34 }
35 } {
36 items [{
37 title "Write some things to do"
38 completed false
39 }]
40 })
41 let numItems = Listlength(items)
42 <div className="app">
43 <div className="title">
44 (str("What to do"))
45 <button onClick=((_evt) dispatch(AddItem))>
46 (str("Add something"))
47 </button>
48 </div>
49 <div className="items">
50 (
51 Reactarray(Arrayof_list(
52 Listmap((item) <TodoItem item /> items)
53 ))
54 )
55 </div>
56 <div className="footer">
57 (str(string_of_int(numItems) " items"))
58 </div>
59 </div>
60 }
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) => {}
wherec
was anint
andd
was astring
, you could call itmyfn(~b=2, ~a=1, 3, "hi")
ormyfn(~a=3, 3, "hi", ~b=1)
but notmyfn(~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:
2 title string
3 completed bool
4 }
5
6 let str = Reactstring
7
8 TodoItem = {
9 [@react.component]
10 let make = (~item) {
11 <div className="item">
12 <input
13 type_="checkbox"
14 checked=(itemcompleted)
15 /* TODO make interactive */
16 />
17 (str(itemtitle))
18 </div>
19 }
20 }
21
22 type state = {items list(item)}
23
24 type action =
25 AddItem
26
27 let newItem = () {title "Click a button" completed true}
28
29 [@react.component]
30 let make = () {
31 let ({items} dispatch) = ReactuseReducer((state action) {
32 switch action {
33 AddItem => {items [newItem() ...stateitems]}
34 }
35 } {
36 items [{
37 title "Write some things to do"
38 completed false
39 }]
40 })
41 let numItems = Listlength(items)
42 <div className="app">
43 <div className="title">
44 (str("What to do"))
45 <button onClick=((_evt) dispatch(AddItem))>
46 (str("Add something"))
47 </button>
48 </div>
50 (
51 Reactarray(Arrayof_list(
52 Listmap((item) <TodoItem item /> items)
53 ))
54 )
55 </div>
57 (str(string_of_int(numItems) " items"))
58 </div>
59 </div>
60 }
In the center of all this you can see the function that takes our data and renders a react element.
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} />
.
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
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.
2 id int
3 title string
4 completed bool
5 }
7 let str = Reactstring
8
9 TodoItem = {
10 [@react.component]
11 let make = (~item) {
12 <div className="item">
13 <input
14 type_="checkbox"
15 checked=(itemcompleted)
16 /* TODO make interactive */
17 />
18 (str(itemtitle))
19 </div>
20 }
21 }
22
23 type state = {items list(item)}
24
25 type action =
26 AddItem
27
28 let lastId = 0
29 let newItem = () {
30 let lastId = lastId 1
31 {id lastId title "Click a button" completed true}
32 }
33
34 [@react.component]
35 let make = () {
36 let ({items} dispatch) = ReactuseReducer((state action) {
37 switch action {
38 AddItem => {items [newItem() ...stateitems]}
39 }
41 items [{
42 id 0
43 title "Write some things to do"
44 completed false
45 }]
46 })
48 <div className="app">
49 <div className="title">
50 (str("What to do"))
51 <button onClick=((_evt) dispatch(AddItem))>
52 (str("Add something"))
53 </button>
54 </div>
55 <div className="items">
56 (
57 Reactarray(Arrayof_list(
58 Listmap((item) <TodoItem item /> items)
59 ))
60 )
61 </div>
62 <div className="footer">
63 (str(string_of_int(numItems) " items"))
64 </div>
65 </div>
66 }
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.
2 id int
3 title string
4 completed bool
5 }
6
7 let str = Reactstring
8
9 TodoItem = {
10 [@react.component]
11 let make = (~item) {
12 <div className="item">
13 <input
14 type_="checkbox"
15 checked=(itemcompleted)
16 /* TODO make interactive */
17 />
18 (str(itemtitle))
19 </div>
20 }
21 }
22
23 type state = {items list(item)}
24
25 type action =
26 AddItem
27
29 let newItem = () {
30 let lastId = lastId 1
31 {id lastId title "Click a button" completed true}
32 }
34 [@react.component]
35 let make = () {
36 let ({items} dispatch) = ReactuseReducer((state action) {
37 switch action {
38 AddItem => {items [newItem() ...stateitems]}
39 }
40 } {
41 items [{
42 id 0
43 title "Write some things to do"
44 completed false
45 }]
46 })
47 let numItems = Listlength(items)
48 <div className="app">
49 <div className="title">
50 (str("What to do"))
51 <button onClick=((_evt) dispatch(AddItem))>
52 (str("Add something"))
53 </button>
54 </div>
55 <div className="items">
56 (
57 Reactarray(Arrayof_list(
58 Listmap((item) <TodoItem item /> items)
59 ))
60 )
61 </div>
62 <div className="footer">
63 (str(string_of_int(numItems) " items"))
64 </div>
65 </div>
66 }
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
.
2 id int
3 title string
4 completed bool
5 }
6
7 let str = Reactstring
8
9 TodoItem = {
10 [@react.component]
11 let make = (~item) {
12 <div className="item">
13 <input
14 type_="checkbox"
15 checked=(itemcompleted)
16 /* TODO make interactive */
17 />
18 (str(itemtitle))
19 </div>
20 }
21 }
22
23 type state = {
24 items list(item)
25 }
26 type action =
27 AddItem
28
30 let newItem = () {
31 lastId lastId 1
32 {id lastId title "Click a button" completed true}
33 }
35 [@react.component]
36 let make = () {
37 let ({items} dispatch) = ReactuseReducer((state action) {
38 switch action {
39 AddItem => {items [newItem() ...stateitems]}
40 }
41 } {
42 items [{
43 id 0
44 title "Write some things to do"
45 completed false
46 }]
47 })
48 let numItems = Listlength(items)
49 <div className="app">
50 <div className="title">
51 (str("What to do"))
52 <button onClick=((_evt) dispatch(AddItem))>
53 (str("Add something"))
54 </button>
55 </div>
56 <div className="items">
57 (Reactarray(Arrayof_list(
58 Listmap(
59 (item) <TodoItem
60 key=(string_of_int(itemid))
61 item
62 /> items
63 )
64 )))
65 </div>
66 <div className="footer">
67 (str(string_of_int(numItems) " items"))
68 </div>
69 </div>
70 }
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.
2 id int
3 title string
4 completed bool
5 }
6
7 let str = Reactstring
8
9 TodoItem = {
10 [@react.component]
11 let make = (~item) {
12 <div className="item">
13 <input
14 type_="checkbox"
15 checked=(itemcompleted)
16 /* TODO make interactive */
17 />
18 (str(itemtitle))
19 </div>
20 }
21 }
22
23 type state = {
24 items list(item)
25 }
26 type action =
27 AddItem
28
29 let lastId = (0)
30 let newItem = () {
31 lastId lastId 1
32 {id lastId title "Click a button" completed true}
33 }
34
35 [@react.component]
36 let make = () {
37 let ({items} dispatch) = ReactuseReducer((state action) {
38 switch action {
39 AddItem => {items [newItem() ...stateitems]}
40 }
41 } {
42 items [{
43 id 0
44 title "Write some things to do"
45 completed false
46 }]
47 })
48 let numItems = Listlength(items)
49 <div className="app">
50 <div className="title">
51 (str("What to do"))
52 <button onClick=((_evt) dispatch(AddItem))>
53 (str("Add something"))
54 </button>
55 </div>
56 <div className="items">
57 (Reactarray(Arrayof_list(
58 Listmap(
60 key=(string_of_int(itemid))
61 item
62 /> items
64 )))
65 </div>
66 <div className="footer">
67 (str(string_of_int(numItems) " items"))
68 </div>
69 </div>
70 }
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.
2 id int
3 title string
4 completed bool
5 }
6
7 let str = Reactstring
8
10 [@react.component]
11 let make = (~item ~onToggle) {
12 <div className="item" onClick=((_evt) onToggle())>
14 type_="checkbox"
15 checked=(itemcompleted)
16 /* TODO make interactive */
17 />
18 (str(itemtitle))
19 </div>
20 }
21 }
22
23 type state = {
24 items list(item)
25 }
26 type action =
27 AddItem
28 ToggleItem(int)
29
30 let lastId = (0)
31 let newItem = () {
32 lastId lastId 1
33 {id lastId title "Click a button" completed true}
34 }
35
36 [@react.component]
37 let make = () {
38 let ({items} dispatch) = ReactuseReducer((state action) {
39 switch action {
40 AddItem => {items [newItem() ...stateitems]}
41 ToggleItem(id) =>
42 let items = Listmap(
43 (item)
44 itemid id
45 {...item completed itemcompleted}
46 item,
47 stateitems
48 )
49 {items items}
50 }
51 } {
52 items [{
53 id 0
54 title "Write some things to do"
55 completed false
56 }]
57 })
58 let numItems = Listlength(items)
59 <div className="app">
60 <div className="title">
61 (str("What to do"))
62 <button onClick=((_evt) dispatch(AddItem))>
63 (str("Add something"))
64 </button>
65 </div>
66 <div className="items">
67 (Reactarray(Arrayof_list(
68 Listmap(
69 (item) <TodoItem
70 key=(string_of_int(itemid))
71 onToggle=(() dispatch(ToggleItem(itemid)))
72 item
73 /> items
74 )
75 )))
76 </div>
77 <div className="footer">
78 (str(string_of_int(numItems) " items"))
79 </div>
80 </div>
81 }
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
.
2 id int
3 title string
4 completed bool
5 }
6
7 let str = Reactstring
8
9 TodoItem = {
10 [@react.component]
11 let make = (~item ~onToggle) {
12 <div className="item" onClick=((_evt) onToggle())>
13 <input
14 type_="checkbox"
15 checked=(itemcompleted)
16 /* TODO make interactive */
17 />
18 (str(itemtitle))
19 </div>
20 }
21 }
22
23 type state = {
24 items list(item)
25 }
27 AddItem
28 ToggleItem(int)
30 let lastId = (0)
31 let newItem = () {
32 lastId lastId 1
33 {id lastId title "Click a button" completed true}
34 }
35
36 [@react.component]
37 let make = () {
38 let ({items} dispatch) = ReactuseReducer((state action) {
40 AddItem => {items [newItem() ...stateitems]}
41 ToggleItem(id) =>
42 let items = Listmap(
43 (item)
44 itemid id
45 {...item completed itemcompleted}
46 item,
47 stateitems
48 )
49 {items items}
50 }
52 items [{
53 id 0
54 title "Write some things to do"
55 completed false
56 }]
57 })
58 let numItems = Listlength(items)
59 <div className="app">
60 <div className="title">
61 (str("What to do"))
62 <button onClick=((_evt) dispatch(AddItem))>
63 (str("Add something"))
64 </button>
65 </div>
66 <div className="items">
67 (Reactarray(Arrayof_list(
68 Listmap(
70 key=(string_of_int(itemid))
71 onToggle=(() dispatch(ToggleItem(itemid)))
72 item
73 /> items
75 )))
76 </div>
77 <div className="footer">
78 (str(string_of_int(numItems) " items"))
79 </div>
80 </div>
81 }
Let's look a little closer at the way we're handling ToggleItem
:
2 id int
3 title string
4 completed bool
5 }
6
7 let str = Reactstring
8
9 TodoItem = {
10 [@react.component]
11 let make = (~item ~onToggle) {
12 <div className="item" onClick=((_evt) onToggle())>
13 <input
14 type_="checkbox"
15 checked=(itemcompleted)
16 /* TODO make interactive */
17 />
18 (str(itemtitle))
19 </div>
20 }
21 }
22
23 type state = {
24 items list(item)
25 }
26 type action =
27 AddItem
28 ToggleItem(int)
29
30 let lastId = (0)
31 let newItem = () {
32 lastId lastId 1
33 {id lastId title "Click a button" completed true}
34 }
35
36 [@react.component]
37 let make = () {
38 let ({items} dispatch) = ReactuseReducer((state action) {
39 switch action {
40 AddItem => {items [newItem() ...stateitems]}
42 let items = Listmap(
43 (item)
44 itemid id
45 {...item completed itemcompleted}
46 item,
47 stateitems
48 )
49 {items items}
51 } {
52 items [{
53 id 0
54 title "Write some things to do"
55 completed false
56 }]
57 })
58 let numItems = Listlength(items)
59 <div className="app">
60 <div className="title">
61 (str("What to do"))
62 <button onClick=((_evt) dispatch(AddItem))>
63 (str("Add something"))
64 </button>
65 </div>
66 <div className="items">
67 (Reactarray(Arrayof_list(
68 Listmap(
69 (item) <TodoItem
70 key=(string_of_int(itemid))
71 onToggle=(() dispatch(ToggleItem(itemid)))
72 item
73 /> items
74 )
75 )))
76 </div>
77 <div className="footer">
78 (str(string_of_int(numItems) " items"))
79 </div>
80 </div>
81 }
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.
2 id int
3 title string
4 completed bool
5 }
6
7 let str = Reactstring
8
9 TodoItem = {
10 [@react.component]
11 let make = (~item ~onToggle) {
12 <div className="item" onClick=((_evt) onToggle())>
13 <input
14 type_="checkbox"
15 checked=(itemcompleted)
16 /* TODO make interactive */
17 />
18 (str(itemtitle))
19 </div>
20 }
21 }
22
23 let valueFromEvent = (evt) string evtReactEventFormtargetvalue
24
26 type state = string
27 [@react.component]
28 let make = (~onSubmit) {
29 let (text setText) = ReactuseReducer((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 (ReactEventKeyboardkey(evt) "Enter") {
37 onSubmit(text)
38 setText("")
39 }
40 )
41 />
42 }
43 }
45 type state = {
46 items list(item)
47 }
48 type action =
49 AddItem(string)
50 ToggleItem(int)
51
52 let lastId = (0)
53 let newItem = (text) {
54 lastId lastId 1
55 {id lastId title text completed false}
56 }
57
58 [@react.component]
59 let make = () {
60 let ({items} dispatch) = ReactuseReducer((state action) {
61 switch action {
62 AddItem(text) => {items [newItem(text) ...stateitems]}
63 ToggleItem(id) =>
64 let items = Listmap(
65 (item)
66 itemid id
67 {...item completed itemcompleted}
68 item,
69 stateitems
70 )
71 {items items}
72 }
73 } {
74 items [{
75 id 0
76 title "Write some things to do"
77 completed false
78 }]
79 })
80 let numItems = Listlength(items)
81 <div className="app">
82 <div className="title">
83 (str("What to do"))
84 <Input onSubmit=((text) dispatch(AddItem(text))) />
85 </div>
86 <div className="items">
87 (Reactarray(Arrayof_list(
88 Listmap(
89 (item) <TodoItem
90 key=(string_of_int(itemid))
91 onToggle=(() dispatch(ToggleItem(itemid)))
92 item
93 /> items
94 )
95 )))
96 </div>
97 <div className="footer">
98 (str(string_of_int(numItems) " items"))
99 </div>
100 </div>
101 }
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 calldispatch
setText
.
Most of this we've seen before, but the onChange
and onKeyDown
handlers are new.
2 id int
3 title string
4 completed bool
5 }
6
7 let str = Reactstring
8
9 TodoItem = {
10 [@react.component]
11 let make = (~item ~onToggle) {
12 <div className="item" onClick=((_evt) onToggle())>
13 <input
14 type_="checkbox"
15 checked=(itemcompleted)
16 /* TODO make interactive */
17 />
18 (str(itemtitle))
19 </div>
20 }
21 }
22
23 let valueFromEvent = (evt) string evtReactEventFormtargetvalue
24
25 Input = {
26 type state = string
27 [@react.component]
28 let make = (~onSubmit) {
29 let (text setText) = ReactuseReducer((oldText newText) newText "")
30 <input
31 value=text
32 type_="text"
33 placeholder="Write something to do"
35 onKeyDown=((evt)
36 if (ReactEventKeyboardkey(evt) "Enter") {
37 onSubmit(text)
38 setText("")
39 }
40 )
42 }
43 }
44
45 type state = {
46 items list(item)
47 }
48 type action =
49 AddItem(string)
50 ToggleItem(int)
51
52 let lastId = (0)
53 let newItem = (text) {
54 lastId lastId 1
55 {id lastId title text completed false}
56 }
57
58 [@react.component]
59 let make = () {
60 let ({items} dispatch) = ReactuseReducer((state action) {
61 switch action {
62 AddItem(text) => {items [newItem(text) ...stateitems]}
63 ToggleItem(id) =>
64 let items = Listmap(
65 (item)
66 itemid id
67 {...item completed itemcompleted}
68 item,
69 stateitems
70 )
71 {items items}
72 }
73 } {
74 items [{
75 id 0
76 title "Write some things to do"
77 completed false
78 }]
79 })
80 let numItems = Listlength(items)
81 <div className="app">
82 <div className="title">
83 (str("What to do"))
84 <Input onSubmit=((text) dispatch(AddItem(text))) />
85 </div>
86 <div className="items">
87 (Reactarray(Arrayof_list(
88 Listmap(
89 (item) <TodoItem
90 key=(string_of_int(itemid))
91 onToggle=(() dispatch(ToggleItem(itemid)))
92 item
93 /> items
94 )
95 )))
96 </div>
97 <div className="footer">
98 (str(string_of_int(numItems) " items"))
99 </div>
100 </div>
101 }
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.
2 id int
3 title string
4 completed bool
5 }
6
7 let str = Reactstring
8
9 TodoItem = {
10 [@react.component]
11 let make = (~item ~onToggle) {
12 <div className="item" onClick=((_evt) onToggle())>
13 <input
14 type_="checkbox"
15 checked=(itemcompleted)
16 /* TODO make interactive */
17 />
18 (str(itemtitle))
19 </div>
20 }
21 }
22
25 Input = {
26 type state = string
27 [@react.component]
28 let make = (~onSubmit) {
29 let (text setText) = ReactuseReducer((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 (ReactEventKeyboardkey(evt) "Enter") {
37 onSubmit(text)
38 setText("")
39 }
40 )
41 />
42 }
43 }
44
45 type state = {
46 items list(item)
47 }
48 type action =
49 AddItem(string)
50 ToggleItem(int)
51
52 let lastId = (0)
53 let newItem = (text) {
54 lastId lastId 1
55 {id lastId title text completed false}
56 }
57
58 [@react.component]
59 let make = () {
60 let ({items} dispatch) = ReactuseReducer((state action) {
61 switch action {
62 AddItem(text) => {items [newItem(text) ...stateitems]}
63 ToggleItem(id) =>
64 let items = Listmap(
65 (item)
66 itemid id
67 {...item completed itemcompleted}
68 item,
69 stateitems
70 )
71 {items items}
72 }
73 } {
74 items [{
75 id 0
76 title "Write some things to do"
77 completed false
78 }]
79 })
80 let numItems = Listlength(items)
81 <div className="app">
82 <div className="title">
83 (str("What to do"))
84 <Input onSubmit=((text) dispatch(AddItem(text))) />
85 </div>
86 <div className="items">
87 (Reactarray(Arrayof_list(
88 Listmap(
89 (item) <TodoItem
90 key=(string_of_int(itemid))
91 onToggle=(() dispatch(ToggleItem(itemid)))
92 item
93 /> items
94 )
95 )))
96 </div>
97 <div className="footer">
98 (str(string_of_int(numItems) " items"))
99 </div>
100 </div>
101 }
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
orevt |> ReactEvent.Form.target |> (x => x##value)
. You can think of->
kind of like method dispatch ingo
andrust
, where a function gets called with the "thing on the left" as the first argument.
2 id int
3 title string
4 completed bool
5 }
6
7 let str = Reactstring
8
9 TodoItem = {
10 [@react.component]
11 let make = (~item ~onToggle) {
12 <div className="item" onClick=((_evt) onToggle())>
13 <input
14 type_="checkbox"
15 checked=(itemcompleted)
16 /* TODO make interactive */
17 />
18 (str(itemtitle))
19 </div>
20 }
21 }
22
23 let valueFromEvent = (evt) string evtReactEventFormtargetvalue
24
25 Input = {
26 type state = string
27 [@react.component]
28 let make = (~onSubmit) {
29 let (text setText) = ReactuseReducer((oldText newText) newText "")
30 <input
31 value=text
32 type_="text"
33 placeholder="Write something to do"
34 onChange=((evt) setText(valueFromEvent(evt)))
36 if (ReactEventKeyboardkey(evt) "Enter") {
37 onSubmit(text)
38 setText("")
39 }
40 )
42 }
43 }
44
45 type state = {
46 items list(item)
47 }
48 type action =
49 AddItem(string)
50 ToggleItem(int)
51
52 let lastId = (0)
53 let newItem = (text) {
54 lastId lastId 1
55 {id lastId title text completed false}
56 }
57
58 [@react.component]
59 let make = () {
60 let ({items} dispatch) = ReactuseReducer((state action) {
61 switch action {
62 AddItem(text) => {items [newItem(text) ...stateitems]}
63 ToggleItem(id) =>
64 let items = Listmap(
65 (item)
66 itemid id
67 {...item completed itemcompleted}
68 item,
69 stateitems
70 )
71 {items items}
72 }
73 } {
74 items [{
75 id 0
76 title "Write some things to do"
77 completed false
78 }]
79 })
80 let numItems = Listlength(items)
81 <div className="app">
82 <div className="title">
83 (str("What to do"))
84 <Input onSubmit=((text) dispatch(AddItem(text))) />
85 </div>
86 <div className="items">
87 (Reactarray(Arrayof_list(
88 Listmap(
89 (item) <TodoItem
90 key=(string_of_int(itemid))
91 onToggle=(() dispatch(ToggleItem(itemid)))
92 item
93 /> items
94 )
95 )))
96 </div>
97 <div className="footer">
98 (str(string_of_int(numItems) " items"))
99 </div>
100 </div>
101 }
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.
2 id int
3 title string
4 completed bool
5 }
6
7 let str = Reactstring
8
9 TodoItem = {
10 [@react.component]
11 let make = (~item ~onToggle) {
12 <div className="item" onClick=((_evt) onToggle())>
13 <input
14 type_="checkbox"
15 checked=(itemcompleted)
16 /* TODO make interactive */
17 />
18 (str(itemtitle))
19 </div>
20 }
21 }
22
23 let valueFromEvent = (evt) string evtReactEventFormtargetvalue
24
25 Input = {
26 type state = string
27 [@react.component]
28 let make = (~onSubmit) {
29 let (text setText) = ReactuseReducer((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 (ReactEventKeyboardkey(evt) "Enter") {
37 onSubmit(text)
38 setText("")
39 }
40 )
41 />
42 }
43 }
44
45 type state = {
46 items list(item)
47 }
49 AddItem(string)
50 ToggleItem(int)
51
52 let lastId = (0)
53 let newItem = (text) {
54 lastId lastId 1
55 {id lastId title text completed false}
56 }
58 [@react.component]
59 let make = () {
60 let ({items} dispatch) = ReactuseReducer((state action) {
62 AddItem(text) => {items [newItem(text) ...stateitems]}
64 let items = Listmap(
65 (item)
66 itemid id
67 {...item completed itemcompleted}
68 item,
69 stateitems
70 )
71 {items items}
72 }
73 } {
74 items [{
75 id 0
76 title "Write some things to do"
77 completed false
78 }]
79 })
80 let numItems = Listlength(items)
81 <div className="app">
83 (str("What to do"))
84 <Input onSubmit=((text) dispatch(AddItem(text))) />
85 </div>
87 (Reactarray(Arrayof_list(
88 Listmap(
89 (item) <TodoItem
90 key=(string_of_int(itemid))
91 onToggle=(() dispatch(ToggleItem(itemid)))
92 item
93 /> items
94 )
95 )))
96 </div>
97 <div className="footer">
98 (str(string_of_int(numItems) " items"))
99 </div>
100 </div>
101 }
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: