Counter
We’re going build the classic frontend starter app, the counter, using ReasonReact. If you’ve already installed the starter project, you can run the project now:
- Go to the root directory of your melange-for-react-devs-template project
- Run
npm run watch
to start the Melange compiler in watch mode. - In another terminal window, start the Vite dev server by running
npm run serve
. As a side effect, it will open a browser tab pointed to http://localhost:5174/.
The App
component
Open Index.re
and you’ll see this:
module App = {
[@react.component]
let make = () => <div> {React.string("welcome to my app")} </div>;
};
module App = {
[@react.component]
let make = () => <div> {React.string("welcome to my app")} </div>;
};
This is just about the simplest component you can make, but through it, we can start to see some of the key differences of developing with ReasonReact:
- All components must be inside a module
- The
make
function renders the component and returnsReact.element
- We must decorate the
make
function with[@react.component]
- In JSX, we must wrap strings with calls to
React.string
Let’s go over these differences in more detail.
Components are modules
In the example above, a new module named App
is defined. OCaml’s modules are somewhat like JavaScript modules, with one notable difference being that there can be multiples modules inside a single file. For now, you just need to know that all components in ReasonReact are modules, and the name of the component comes from the name of the module.
The make
function
The make
function has the type unit => React.element
, meaning it takes ()
as the only argument and returns an object of type React.element
. You’ll need to decorate make
with the attribute@react.component
. We’ll go into the details later, but for now let’s just say that @react.component
is there to reduce boilerplate and make our code more readable and easier to maintain.
Wrap strings with React.string
Unlike in normal React, we must wrap strings inside function calls to convert them to the React.element
type. This is exactly what the React.string
function does—if you hover over it, you’ll see that it displays the type string => React.element
.
Using the App component
A little bit further down, we make use of the App
component:
let node = ReactDOM.querySelector("#root");
switch (node) {
| None =>
Js.Console.error("Failed to start React: couldn't find the #root element")
| Some(root) =>
let root = ReactDOM.Client.createRoot(root);
ReactDOM.Client.render(root, <App />);
};
let node = ReactDOM.querySelector("#root");
switch (node) {
| None =>
Js.Console.error("Failed to start React: couldn't find the #root element")
| Some(root) =>
let root = ReactDOM.Client.createRoot(root);
ReactDOM.Client.render(root, <App />);
};
React.querySelector("#root")
returns an option(Dom.element)
, meaning that if it doesn’t find the element, it returns None
, and if it does find the element, it returns Some(Dom.element)
, i.e. the element wrapped in the Some
constructor. The switch expression[1] allows you to succinctly express:
- If
node
isNone
, log an error message - Otherwise if
node
isSome(Dom.element)
, render theApp
component to the DOM
We’ll talk more about option
in the Celsius converter chapter.
The Counter component
Let’s create a counter component by creating a new file Counter.re
with the following contents:
[@react.component]
let make = () => {
let (counter, setCounter) = React.useState(() => 0);
<div>
<button onClick={_evt => setCounter(v => v - 1)}>
{React.string("-")}
</button>
{React.string(Int.to_string(counter))}
<button onClick={_evt => setCounter(v => v + 1)}>
{React.string("+")}
</button>
</div>;
};
[@react.component]
let make = () => {
let (counter, setCounter) = React.useState(() => 0);
<div>
<button onClick={_evt => setCounter(v => v - 1)}>
{React.string("-")}
</button>
{React.string(Int.to_string(counter))}
<button onClick={_evt => setCounter(v => v + 1)}>
{React.string("+")}
</button>
</div>;
};
This is a component with a single useState
hook. It should look fairly familiar if you know about hooks in React. Note that we didn’t need to manually define a module for Counter
, because all source files in OCaml are automatically modules, with the name of the module being the same as the name of the file.
Now let’s modify App
so that it uses our new Counter
component:
module App = {
[@react.component]
let make = () => <Counter />;
};
module App = {
[@react.component]
let make = () => <Counter />;
};
The pipe last operator
To display the number of the counter, we wrote {React.string(Int.to_string(counter))}
, which converts an integer to a string, and then converts that string to React.element
. In OCaml, there’s a way to apply a sequence of operations over some data so that it can be read from left to right:
{counter |> Int.to_string |> React.string}
{counter |> Int.to_string |> React.string}
This uses the pipe last operator, which is useful for chaining function calls.
Basic styling
Let’s add a bit of styling to the root element of Counter
:
<div
style={ReactDOM.Style.make(
~padding="1em",
~display="flex",
~gridGap="1em",
(),
)}>
<button onClick={_evt => setCounter(v => v - 1)}>
{React.string("-")}
</button>
<span> {counter |> Int.to_string |> React.string} </span>
<button onClick={_evt => setCounter(v => v + 1)}>
{React.string("+")}
</button>
</div>;
<div
style={ReactDOM.Style.make(
~padding="1em",
~display="flex",
~gridGap="1em",
(),
)}>
<button onClick={_evt => setCounter(v => v - 1)}>
{React.string("-")}
</button>
<span> {counter |> Int.to_string |> React.string} </span>
<button onClick={_evt => setCounter(v => v + 1)}>
{React.string("+")}
</button>
</div>;
Unlike in React, the style
prop in ReasonReact doesn’t take a generic object, instead it takes an object of type ReactDOM.style
that is created by calling ReactDOM.Style.make
. This isn’t a sustainable way to style our app—later, we’ll see how to style using CSS classes.
Congratulations! You’ve created your first ReasonReact app and component. In future chapters we’ll create more complex and interesting components.
Overview
- How to create and run a basic ReasonReact app
- ReasonReact components are also modules
- OCaml has an
option
type whose value can be eitherNone
orSome(_)
- The pipe last operator (
|>
) is an alternate way to invoke functions that enables easy chaining of function calls - The
style
prop doesn’t take generic objects
Exercises
1. What happens if you try to remove the | None
branch of the switch (node)
expression in Index.re
?
switch (node) {
| None =>
Js.Console.error("Failed to start React: couldn't find the #root element")
| Some(root) =>
let root = ReactDOM.Client.createRoot(root);
ReactDOM.Client.render(root, <App />);
};
switch (node) {
| None =>
Js.Console.error("Failed to start React: couldn't find the #root element")
| Some(root) =>
let root = ReactDOM.Client.createRoot(root);
ReactDOM.Client.render(root, <App />);
};
Solution
Removing the | None
branch will result in a compilation error:
Error (warning 8 [partial-match]): this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
None
Error (warning 8 [partial-match]): this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
None
Basically, the compiler is telling you to handle the None
case if you want to ship your app. This is part of what makes OCaml such a type-safe language.
2. What happens if you rename the _evt
variable inside the button callback to evt
?
<button onClick={evt => setCounter(v => v - 1)}>
<button onClick={evt => setCounter(v => v - 1)}>
Solution
Renaming _evt
to evt
results in a compilation error:
Error (warning 27 [unused-var-strict]): unused variable evt.
Error (warning 27 [unused-var-strict]): unused variable evt.
By default, OCaml wants you to use all the variables you declare, unless they begin with _
(underscore).
3. Comment out the [@react.component]
attribute in Counter.re
. What happens?
//[@react.component]
let make = () => {
let (counter, setCounter) = React.useState(() => 0);
//[@react.component]
let make = () => {
let (counter, setCounter) = React.useState(() => 0);
Solution
Commenting out [@react.component]
in Counter.re
will trigger a compilation error in Index.re
, at the place where Counter
component is used:
File "Index.re", line 3, characters 19-27:
3 | let make = () => <Counter />;
^^^^^^^^^^^
Error: Unbound value Counter.makeProps
File "Index.re", line 3, characters 19-27:
3 | let make = () => <Counter />;
^^^^^^^^^^^
Error: Unbound value Counter.makeProps
For now, don’t worry about what Counter.makeProps
is or where it came from—just remember that you need to put the [@react.component]
attribute above your make
function if you want your component to be usable in JSX. This is a very common newbie mistake. See the PPX chapter for more details.
View source code and demo for this chapter.
Despite the name, don’t confuse OCaml’s switch expressions with JavaScript’s switch statements. ↩︎