Tutorial: Build an App with ReasonReact
Part 2—Creating the To-do App
In my previous post, we explored why ReasonReact is awesome. In this post, we’ll roll up our sleeves and basic to-do app with it.
For this project, we will be using these technologies I mentioned previously:
InstallFest!
Before we start coding you’ll need to install a few packages, if you haven’t done so already, including the Reason CLI. Follow the instructions for your specific platform:
- Mac users:
yarn global add reason-cli@latest-macos
- Linux users:
yarn global add reason-cli@latest-linux
- Windows user: Please see this issue thread on GitHub for instructions
Next, we’ll install BuckleScript. Run npm install -g bs-platform
to install BuckleScript globally.
We’ll be using Reason Scripts so we can work in a way that will feel more familiar to React developers like us.
To scaffold your ReasonReact project, run:
yarn create react-app reason-react-todo --scripts-version reason-scripts
Let’s jump in and explore our new project:
cd reason-react-todo && yarn start
It should look something like this. Looks familiar, doesn’t it?
What’s Inside?
Exploring what’s in the boilerplate project, the familiar files and directories to notice here are package.json
, .gitignore
, README.md
, yarn.lock
, public/
, and node_modules/
.
Additionally, you will find:
.merlin
: You should not touch this file. This file is created by the build system and contains absolute paths for Merlin to understand the project layout and provide editor tooling.bsconfig.json
: This is a necessary file used by BuckleScript’s build system, bsb. You can find more details about it here..re files
: These are the Reason files. We will be writing our code in these files. BuckleScript will convert these files into.bs.js
files, automatically.
In App.re
file, we see these lines of code:
[%bs.raw {|require('./App.css')|}];
[@bs.module] external logo : string = "./logo.svg";
That’s how BuckleScript imports the CSS file and the logo file. You can read more on imports and exports with BuckleScript here.
ReasonReact does not use classes. All we have to do to create a component is:
let component = ReasonReact.statelessComponent("MyApp");
Next, we use a make
function to return the component we just created. In that function, we can override a few fields like the render
, and initialState
. The render
field is where we write our JSX:
let component = ReasonReact.statelessComponent("MyApp");let make = (~name, _children) => {
...component, // spread the template's other defaults into here render: _self => <div> {ReasonReact.string(name)} </div>
};
We can see above that the last prop in a make
function is children
. If you don’t need to use the children
prop in your component, you can call it either_
or _children
to avoid compiler warnings.
You may also notice that we’re not using this
. In ReasonReact, we don’t have access to JavaScript’s this
, but rather we use self
which is equivalent to this
for our purposes.
Finally, we use ReasonReact.string
to print strings. ReasonReact.string
takes in a string and returns a reactElement
.
The TodoApp Component
Now that we understand the basic syntax of a ReasonReact component, we’re ready to create our to-do app’s outer-most component:
[%bs.raw {|require('./App.css')|}];
[@bs.module] external logo : string = "./logo.svg";let component = ReasonReact.statelessComponent("App");let make = (~message, _children) => {
...component,
render: _self =>
<div className="App">
<div className="App-header">
<img src=logo className="App-logo" alt="logo" />
<h2> (ReasonReact.string(message)) </h2>
</div>
<div className="App-intro"> <TodoApp /> </div>
</div>,
};
Notice that we’re rendering a TodoApp
component above, but we haven’t created this yet. Let’s create a components
sub-directory in src
directory and create a file in it called TodoApp.re
. This will be our stateful component.
In ReasonReact, typically, a stateful component includes actions, a reducer, and state (just like Redux—but don’t worry, we’re not going to use Redux in this app).
As we know, a state is tracked in an object in React. In Reason, we have two choices do the same, Record, and Object. We will be creating a Record in our app. Let’s see how we can create a Record and how declaring an object is a little different than how we do it in JavaScript. In Reason, we can define an object type.
Note: In ReasonML, there are two types of objects. There are “open” object types (which can contain other values and objects beyond what’s in its type declaration) and “closed” object types (where the object must have the same shape as the declared type). For more information, check the docs.
Since we’re going to use this Record throughout our app, let’s put it in a separate file in the components
directory called TodoModel.re
type item = {
id: int,
title: string,
completed: bool,
};
Back in TodoApp.re
we define the state for our app:
type state = {
items: list(TodoModel.item), // list of Todo items
inputText: string,
// we will use this when we will take input from a user
};
Now, let’s create a stateful component:
// state declaration herelet component = ReasonReact.reducerComponent("TodoLists");let str = ReasonReact.string;let make = _children => {
...component,
initialState: () => {
items: [{id: 0, title: "milk", completed: false}],
inputText: "",
},
reducer: ((), _) => ReasonReact.NoUpdate,
// we will come back to this when we have actions.
// Currently, we do not want our state to change since we just
// want to print data on screen. render: self => {
let {items, inputText} = self.state; // destructure the state
<div className="app">
<div className="list">
(
ReasonReact.array(
// converts an array into a react element
Array.of_list(
// returns a fresh array containing elements of a list
List.map(
// returns a new list
(item: TodoModel.item) =>
<TodoItem
key=(string_of_int(item.id))
// converts int id into string id
item
/>,
items,
),
),
)
)
</div>
</div>;
},
}
Finally we’ll build theTodoItem
component. It is going to be a stateless component because its job is to display data.
Write the follow in in a new file calledTodoItem.re
let str = ReasonReact.string;let component = ReasonReact.statelessComponent("TodoItem");let make = (~item: TodoModel.item, _children) => {
...component,
render: _self =>
<div className="item">
<input
_type="checkbox"
checked=item.completed
/>
<p> (str(item.title)) </p>
</div>,
};
To let BuckleScript access our .re
files and convert them into .bs.js
files, we need to edit our bsconfig.js
file.
"sources": ["src", "src/components"]
stop the server, run the command bsb
at the root of the project dir and start the project again withyarn start
and we should be able to see the following result.
It works! Woohoo! Congratulations! That’s the basics of how to write stateless and stateful components in ReasonReact.
We can also add a header component to our app by creating a TodoHeader.re
file and adding the following code:
let component = ReasonReact.statelessComponent("TodoHeader");let make = _children => {
...component,
render: _self =>
<div className="app-header">
<div className="title">
(ReasonReact.string("Todo List"))
</div>
</div>,
};
Include TodoHeader
component in TodoApp.re
// ...
render: self => {
let {items, inputText} = self.state;
<div className="app">
// add this line
<TodoHeader />
... </div>
}
// ...
Nice, we can see our App Header now.
Now, let’s make our app more interactive by adding 2 things:
- A checkbox toggle
- A button to remove a particular to-do item
type action =
| Toggle(int)
| RemoveItem(int);
let make = _children => {
...component,
initialState: () => {
items: [{id: 0, title: "milk", completed: false}],
inputText: "",
},
reducer: action =>
switch (action) {
| Toggle(id) => (
state =>
ReasonReact.Update({
...state,
items:
List.map(
(item: TodoModel.item) =>
item.id == id ?
{
...item,
TodoModel.completed:!TodoModel(item.completed),
} :
item,
state.items,
),
})
)
| RemoveItem(id) => (
state =>
ReasonReact.Update({
...state,
items:
List.filter(
(item: TodoModel.item) => item.id !== id,
state.items,
),
})
)
},
render: self => {
...
<div className="list">
(
ReasonReact.array(
Array.of_list(
List.map(
(item: TodoModel.item) =>
<TodoItem
key=(string_of_int(item.id))
item
onRemove=(id => self.send(RemoveItem(id)))
onToggle=(id => self.send(Toggle(id)))
/>,
items,
),
),
)
)
</div>
}
This is our first step towards actions and reducers.
Now, refactor TodoItem.re
let str = ReasonReact.string;
let component = ReasonReact.statelessComponent("TodoItem");
let make = (~item: TodoModel.item, ~onRemove, ~onToggle, _children) => {
...component,
render: _self =>
<div className="item">
<input
_type="checkbox"
checked=item.completed
onChange=((_) => onToggle(item.id))
/>
<p> (str(item.title)) </p>
<button onClick=((_) => onRemove(item.id))>
(str("Remove"))
</button>
</div>,
};
This code enables us to change our app’s state using a checkbox or a remove button attached to each todo item.
Challenge Round: Create a TodoFooter
component that can show a total number of items, has a button to remove all the items and has another button to remove completed items. At the end it should look something like this.
You can find code for the complete project here: https://github.com/redacademy/reason-react-todo).
Did you finish the challenge? Nice work!
Moving on, we have to create an input component so our user can add todos. Refactor TodoApp.re
type action =
| InputText(string)
| Toggle(int)
| RemoveAll
| RemoveItem(int)
| RemoveCompleted
| Submit;let component = ReasonReact.reducerComponent("TodoApp");let str = ReasonReact.string;let make = _children => {
let handleSubmit = state => {
let newId: int = List.length(state.items);
let newItem: TodoModel.item = {
id: newId,
title: state.inputText,
completed: false,
};
let newList = [newItem, ...state.items];
ReasonReact.Update({items: newList, inputText: ""});
};
{
...
reducer: action =>
switch (action) {
| InputText(newText) => (
state => ReasonReact.Update({...state, inputText:newText})
)
| Submit => (state => handleSubmit(state))
},
render: self => {
let {items, inputText} = self.state;
<div className="app">
<TodoHeader />
<AddTodo
submit=((_) => self.send(Submit))
value=inputText
onInputText=(text => self.send(InputText(text)))
/>
...
</div>
}
You might have noticed that we do not have an AddTodo
component yet. Let’s create that component in a file called AddTodo.re
let str = ReasonReact.string;let component = ReasonReact.statelessComponent("AddTodo");let make = (~value, ~onInputText, ~submit, _children) => {
...component,
render: _self =>
<div className="input">
<input
value
placeholder="add item"
onChange=(
event =>
onInputText( ReactDOMRe.domElementToObj(ReactEventRe.Form.target(event))##value,
)
)
/>
<button onClick=((_) => submit())> (str("Add")) </button>
</div>,
};
What is ReactDomRe
?
ReactDomRe
is ReasonReact’s React-DOM module. ReactDOMRe.domElementToObj(ReactEventRe.Form.target(event))##value
is the same as event.target.value
in JavaScript.
You can find out more about theReactDomRe
module here: (https://reasonml.github.io/reason-react/docs/en/dom.html#reactdom)
We’re finished! In the end, our app looks like this. If you’re project is not working, you can have a look at the GitHub repo for this project.
If you liked this post and want to keep learning about web and app development with RED Academy, be sure to follow us on Medium or check out our website.