Skip to content

Order Confirmation

The famed restauranteur Madame Jellobutter has opened a hot new pop-up restaurant called Emoji Cafe, and you’ve been commissioned to build the order confirmation widget on its website. Feeling adventurous, you decide to build it using Melange.

Start by creating a new directory src/order-confirmation and give it the same directory structure as we showed you in the previous chapter:

src/order-confirmation
├─ dune
├─ Index.re
├─ Item.re
└─ index.html
src/order-confirmation
├─ dune
├─ Index.re
├─ Item.re
└─ index.html

The dune file can be copied from any of the existing projects. The index.html file can also be copied over, but remember to update the value of the script element’s src attribute to point to the new location of Index.js:

../../_build/default/src/order-confirmation/output/src/order-confirmation/Index.js
../../_build/default/src/order-confirmation/output/src/order-confirmation/Index.js

The .re files can be empty for now.

Variant type Item.t

For the time being, there are only two items you can order at Emoji Cafe, the sandwich or the burger. In Item.re, add a new type:

re
type t =
  | Sandwich
  | Burger;
type t =
  | Sandwich
  | Burger;

This is a variant type[1] named t with two constructors, Sandwich and Burger. In OCaml, it is customary for the primary type of a module to be called t. This convention makes sense because in other modules, this type will be referred to as Item.t.

The Item module should contain helper functions that return the price and the emoji[2] for a given item. First, add the toPrice function:

re
let toPrice = t =>
  switch (t) {
  | Sandwich => 10.
  | Burger => 15.
  };
let toPrice = t =>
  switch (t) {
  | Sandwich => 10.
  | Burger => 15.
  };

If Madame Jellobutter decides to add a hotdog to the menu, you would need to:

  • Add a Hotdog constructor to Item.t
  • Add a | Hotdog branch to the switch expression of Item.toPrice

Your OCaml code would fail to compile if you added Hotdog or removed Sandwich from Item.t without also updating Item.toPrice. This is one of the great advantages of variant types: changing the constructors will force you to change the relevant parts of your code.

Wildcard in switch expressions

If Madame Jellobutter decides to do a promotion that lowers the price of burgers so that they’re the same price as sandwiches, you could rewrite Item.toPrice to:

reason
let toPrice = t =>
  switch (t) {
  | _ => 10.
  };
let toPrice = t =>
  switch (t) {
  | _ => 10.
  };

The underscore (_) here serves as a wildcard matching any constructor. However, this would be a very bad idea! Now changing the constructors in Item.t would not force you to change Item.toPrice accordingly. A much better version would be:

reason
let toPrice = t =>
  switch (t) {
  | Sandwich => 10.
  | Burger => 10.
  };
let toPrice = t =>
  switch (t) {
  | Sandwich => 10.
  | Burger => 10.
  };

Since OCaml’s pattern-matching syntax allows you to combine branches, you can simplify it to:

reason
let toPrice = t =>
  switch (t) {
  | Sandwich
  | Burger => 10.
  };
let toPrice = t =>
  switch (t) {
  | Sandwich
  | Burger => 10.
  };

In any case, you should strive to avoid wildcards. The OCaml Way is to explicitly match all constructors in your switch expressions.

A fun syntax for switch

There’s an alternate, shorter syntax for functions whose entire body is a switch expression. It’s called fun, and we can rewrite Item.toPrice to use it:

re
let toPrice =
  fun
  | Sandwich => 10.
  | Burger => 15.;
let toPrice =
  fun
  | Sandwich => 10.
  | Burger => 15.;

We can also define toEmoji using the fun syntax:

re
let toEmoji =
  fun
  | Sandwich => {js|🥪|js}
  | Burger => {js|🍔|js};
let toEmoji =
  fun
  | Sandwich => {js|🥪|js}
  | Burger => {js|🍔|js};

Using the fun syntax is completely equivalent to using a switch expression, so it’s up to your personal taste whether you want to use one or the other.

Rendering an item

Now we’re ready to define the Item.make function which will render the Item component:

re
[@react.component]
let make = (~item: t) =>
  <tr>
    <td> {item |> toEmoji |> React.string} </td>
    <td>
      {item |> toPrice |> Js.Float.toFixed(~digits=2) |> React.string}
    </td>
  </tr>;
[@react.component]
let make = (~item: t) =>
  <tr>
    <td> {item |> toEmoji |> React.string} </td>
    <td>
      {item |> toPrice |> Js.Float.toFixed(~digits=2) |> React.string}
    </td>
  </tr>;

The Item.make function has a single labeled argument, ~item, of type Item.t. This effectively means the Item component has a single prop named item.

Note that this renders a single row of a table. We’ll need another component to render a table containing all items in an order.

Order component

Create a new file src/order-confirmation/Order.re and add the following code:

re
type t = array(Item.t);

[@react.component]
let make = (~items: t) => {
  let total =
    items
    |> Js.Array.reduce(~init=0., ~f=(acc, order) =>
         acc +. Item.toPrice(order)
       );

  <table>
    <tbody>
      {items |> Js.Array.map(~f=item => <Item item />) |> React.array}
      <tr>
        <td> {React.string("Total")} </td>
        <td> {total |> Js.Float.toFixed(~digits=2) |> React.string} </td>
      </tr>
    </tbody>
  </table>;
};
type t = array(Item.t);

[@react.component]
let make = (~items: t) => {
  let total =
    items
    |> Js.Array.reduce(~init=0., ~f=(acc, order) =>
         acc +. Item.toPrice(order)
       );

  <table>
    <tbody>
      {items |> Js.Array.map(~f=item => <Item item />) |> React.array}
      <tr>
        <td> {React.string("Total")} </td>
        <td> {total |> Js.Float.toFixed(~digits=2) |> React.string} </td>
      </tr>
    </tbody>
  </table>;
};

There’s a lot going on here:

  • The primary type of the Order module is array(Item.t), which is an array of variants.
  • The Order.make function has a single labeled argument, ~items, of type Order.t. This means the Order component has a single prop named items.
  • We sum up the prices of all items using Js.Array.reduce, which is the Melange binding to JavaScript’s Array.reduce method. Note that Js.Array.reduce requires the initial value to be passed in.
  • For each order, we render an Item component via Js.Array.map, which is the Melange binding to the Array.map method.
  • There’s a call to React.array, which we’ll address in a little bit.

Rendering an order in Index.re

Render the Order component inside src/order-confirmation/Index.re:

re
module App = {
  let items: Order.t = [|Sandwich, Burger, Sandwich|];

  [@react.component]
  let make = () =>
    <div>
      <h1> {React.string("Order confirmation")} </h1>
      <Order items />
    </div>;
};

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 />);
};
module App = {
  let items: Order.t = [|Sandwich, Burger, Sandwich|];

  [@react.component]
  let make = () =>
    <div>
      <h1> {React.string("Order confirmation")} </h1>
      <Order items />
    </div>;
};

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 />);
};

Run npm run serve inside src/order-confirmation to see your new app in action.

Passing key prop to Items

Open your browser’s dev console, where you should see a warning:

Warning: Each child in a list should have a unique "key" prop.
Warning: Each child in a list should have a unique "key" prop.

Oops, we forgot the set the key prop! One way to fix this is to use Js.Array.mapi instead[3] so we can set key based on the index of the element:

re
items
|> Js.Array.mapi(~f=(item, index) =>
     <Item key={"item-" ++ string_of_int(index)} item />
   )
|> React.array
items
|> Js.Array.mapi(~f=(item, index) =>
     <Item key={"item-" ++ string_of_int(index)} item />
   )
|> React.array

The Js.Array.mapi function is also a binding to the Array.map method, but unlike Js.Array.map, it passes the element and the index into the callback. If you hover over it, you’ll see that it has the type signature

(('a, int) => 'b, array('a)) => array('b)
(('a, int) => 'b, array('a)) => array('b)

When a JavaScript function has optional arguments, it’s common to create multiple OCaml functions that bind to it. We’ll discuss this in more detail later.

Type transformations in JSX

As mentioned before, there’s a call to React.array after the call to Js.Array.map:

reason
{items |> Js.Array.map(item => <Item item />) |> React.array}
{items |> Js.Array.map(item => <Item item />) |> React.array}

If we leave off the call to React.array, we get this error:

File "src/order-confirmation/Order.re", lines 12, characters 6-12:
12 |         {items |> Js.Array.map(item => <Item item />)}
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Error: This expression has type React.element array
       but an expression was expected of type React.element
File "src/order-confirmation/Order.re", lines 12, characters 6-12:
12 |         {items |> Js.Array.map(item => <Item item />)}
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Error: This expression has type React.element array
       but an expression was expected of type React.element

The compiler is essentially informing us that the tbody element expects children of type React.element[4], but the call to Js.Array.map returns array(React.element), which creates a type mismatch. To make the actual type match the expected type, we must add a call to React.array which turns array(React.element) into React.element.

To better see what types are at play, it might make sense to refactor Order.make like so:

re
let total =
  items
  |> Js.Array.reduce(~init=0., ~f=(acc, order) =>
       acc +. Item.toPrice(order)
     );

let itemRows: array(React.element) =
  items |> Js.Array.map(~f=item => <Item item />);

<table>
  <tbody>
    {itemRows |> React.array}
    <tr>
      <td> {React.string("Total")} </td>
      <td> {total |> Js.Float.toFixed(~digits=2) |> React.string} </td>
    </tr>
  </tbody>
</table>;
let total =
  items
  |> Js.Array.reduce(~init=0., ~f=(acc, order) =>
       acc +. Item.toPrice(order)
     );

let itemRows: array(React.element) =
  items |> Js.Array.map(~f=item => <Item item />);

<table>
  <tbody>
    {itemRows |> React.array}
    <tr>
      <td> {React.string("Total")} </td>
      <td> {total |> Js.Float.toFixed(~digits=2) |> React.string} </td>
    </tr>
  </tbody>
</table>;

React.array is a strict type transformation that doesn’t actually change the underlying JavaScript object. For example, try running the following code in the playground:

re
let elemArray: array(React.element) =
  [|"a", "b", "c"|] |> Js.Array.map(~f=x => React.string(x));
Js.log(elemArray);
Js.log(React.array(elemArray));
let elemArray: array(React.element) =
  [|"a", "b", "c"|] |> Js.Array.map(~f=x => React.string(x));
Js.log(elemArray);
Js.log(React.array(elemArray));

If you look at the JavaScript output, you’ll see that the two calls to Js.log get compiled to

javascript
console.log(elemArray);
console.log(elemArray);
console.log(elemArray);
console.log(elemArray);

The call to React.array in OCaml was erased in the JavaScript output!

Besides React.array, the other type transformation functions in ReasonReact are React.string (which you’ve already seen), React.int, and React.float. To see why they are necessary, consider this small example in ReactJS:

javascript
let answer = (value > 100) ? "Too large" : value;
return (<div> {answer} </div>);
let answer = (value > 100) ? "Too large" : value;
return (<div> {answer} </div>);

The type of answer is either a string or a Number, and for ReactJS, it’s no problem because JavaScript is dynamically typed. However, we would run into a problem if we tried to directly translate this example to OCaml:

reason
let answer = value > 100 ? "Too large" : value;
<div> answer </div>;
let answer = value > 100 ? "Too large" : value;
<div> answer </div>;

We could get a compilation error like this:

2 |   let answer = value > 100 ? "Too large" : value;
                                               ^^^^^
Error: This expression has type int but an expression was expected of type
         string
2 |   let answer = value > 100 ? "Too large" : value;
                                               ^^^^^
Error: This expression has type int but an expression was expected of type
         string

A value in OCaml can only be one type, regardless of which branch succeeds. Therefore we must use type transformation functions to “unify” the type of answer to React.element:

reason
let answer: React.element =
  value > 100 ? React.string("Too large") : React.int(value);
<div> answer </div>;
let answer: React.element =
  value > 100 ? React.string("Too large") : React.int(value);
<div> answer </div>;

Type transformation functions have another role: They limit the types we can pass into our JSX. For example, there’s no React.object function, so there’s no easy way to put an object directly into ReasonReact JSX. In ReactJS, this is easy to do, and it would cause your component to error out.


Wunderbar! You’ve got a basic order confirmation component, but it looks… not so great[5]. In the next chapter, we’ll see how ReasonReact components can be styled with plain old CSS.

Overview

  • By convention, the main type in a module is often named t
  • A variant is a type that has one or more constructors
    • Adding or removing constructors forces you to change the relevant parts of your code, unless you use wildcards when pattern-matching on a variant
    • Using wildcards in your switch expression makes your code less adaptable to change
  • The fun syntax helps you save a little bit of typing when you have a function whose entire body is a switch expression
  • Labeled arguments in a component’s make function are treated as props by ReasonReact.
  • The Js.Array module contains useful array functions
    • The Js.Array.reduce function is the binding to JavaScript’s Array.reduce method
    • The Js.Array.map and Js.Array.mapi functions are both bindings to JavaScript’s Array.map method
  • The React.array function is needed when you want to convert an array of React.elements to a single React.element, e.g. after a call to Js.Array.map

Exercises

1. The Item component is only used inside the Order component and we don’t expect it to be used anywhere else (items rendered in a menu component would look different). Rename it to OrderItem and move it inside the Order module.

Hint

Create a submodule inside Order.re

Solution

To move the Item component from the Item module to the Order module, you’ll have to move the Item.make function to a submodule called Order.OrderItem. Then you’ll have to prefix the references to t, toPrice, and toEmoji with Item. since they’re now being referenced outside the Item module. After you’re done, src/order-confirmation/Order.re should look something like this:

re
type t = array(Item.t);

module OrderItem = {
  [@react.component]
  let make = (~item: Item.t) =>
    <tr>
      <td> {item |> Item.toEmoji |> React.string} </td>
      <td> {item |> Item.toPrice |> Format.currency} </td>
    </tr>;
};

[@react.component]
let make = (~items: t) => {
  let total =
    items
    |> Js.Array.reduce(~init=0., ~f=(acc, order) =>
         acc +. Item.toPrice(order)
       );

  <table>
    <tbody>
      {items
       |> Js.Array.mapi(~f=(item, index) =>
            <OrderItem key={"item-" ++ string_of_int(index)} item />
          )
       |> React.array}
      <tr>
        <td> {React.string("Total")} </td>
        <td> {total |> Format.currency} </td>
      </tr>
    </tbody>
  </table>;
};
type t = array(Item.t);

module OrderItem = {
  [@react.component]
  let make = (~item: Item.t) =>
    <tr>
      <td> {item |> Item.toEmoji |> React.string} </td>
      <td> {item |> Item.toPrice |> Format.currency} </td>
    </tr>;
};

[@react.component]
let make = (~items: t) => {
  let total =
    items
    |> Js.Array.reduce(~init=0., ~f=(acc, order) =>
         acc +. Item.toPrice(order)
       );

  <table>
    <tbody>
      {items
       |> Js.Array.mapi(~f=(item, index) =>
            <OrderItem key={"item-" ++ string_of_int(index)} item />
          )
       |> React.array}
      <tr>
        <td> {React.string("Total")} </td>
        <td> {total |> Format.currency} </td>
      </tr>
    </tbody>
  </table>;
};

2. Add another constructor to Item.t variant type. Update the Item module’s helper functions to get your program to compile again.

Solution

Let’s say you add a HotDog constructor to Item.t; your Item module would look something like this:

re
type t =
  | Sandwich
  | Burger
  | Hotdog;

let toPrice =
  fun
  | Sandwich => 10.
  | Burger => 15.
  | Hotdog => 5.;

let toEmoji =
  fun
  | Sandwich => {js|🥪|js}
  | Burger => {js|🍔|js}
  | Hotdog => {js|🌭|js};
type t =
  | Sandwich
  | Burger
  | Hotdog;

let toPrice =
  fun
  | Sandwich => 10.
  | Burger => 15.
  | Hotdog => 5.;

let toEmoji =
  fun
  | Sandwich => {js|🥪|js}
  | Burger => {js|🍔|js}
  | Hotdog => {js|🌭|js};

Of course, you may have chosen a different price for the hotdog. Or maybe you didn’t add a hotdog at all, and instead added CannedFood (🥫) or PotOfFood (🍲). It’s totally up to you!

3. Instead of repeatedly using value |> Js.Float.toFixed(~digits=2) |> React.string, add a helper function Format.currency that does the same thing.

Hint

Create a new file named Format.re.

Solution

In order to create a helper function Format.currency, we must create a new module file called Format.re and add a currency function:

re
let currency = value => value |> Js.Float.toFixed(~digits=2) |> React.string;
let currency = value => value |> Js.Float.toFixed(~digits=2) |> React.string;

Then we can use that function like this:

reason
<td> {item |> toPrice |> Format.currency} </td>
<td> {item |> toPrice |> Format.currency} </td>

View source code and demo for this chapter.



  1. Variant types have no equivalent in JavaScript, but they are similar to TypeScript’s union enums ↩︎

  2. Recall that the name of this restaurant is Emoji Cafe, so everything from the menu to the order confirmation must use emojis. ↩︎

  3. Using array indexes to set keys violates React’s rules of keys, which states that you shouldn’t generate keys while rendering. We’ll see a better way to do this later. ↩︎

  4. All lowercase elements like div, span, table, etc expect their children to be of type React.element. But React components (with uppercase names) can take children of any type. ↩︎

  5. Madame Jellobutter was passing by and just happened to catch a glimpse of the unstyled component over your shoulder and puked in her mouth a little. ↩︎