Skip to content

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:

  1. Go to the root directory of your melange-for-react-devs-template project
  2. Run npm run watch to start the Melange compiler in watch mode.
  3. 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:

re
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 returns React.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:

re
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 is None, log an error message
  • Otherwise if node is Some(Dom.element), render the App 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:

re
[@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:

re
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:

reason
{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:

re
<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 either None or Some(_)
  • 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?

reason
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?

reason
<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?

reason
//[@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.



  1. Despite the name, don’t confuse OCaml’s switch expressions with JavaScript’s switch statements. ↩︎