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.htmlsrc/order-confirmation
├─ dune
├─ Index.re
├─ Item.re
└─ index.htmlThe 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.jsThe .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:
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:
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
Hotdogconstructor toItem.t - Add a
| Hotdogbranch to the switch expression ofItem.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:
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:
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:
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:
let toPrice =
fun
| Sandwich => 10.
| Burger => 15.;let toPrice =
fun
| Sandwich => 10.
| Burger => 15.;We can also define toEmoji using the fun syntax:
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:
[@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:
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
Ordermodule isarray(Item.t), which is an array of variants. - The
Order.makefunction has a single labeled argument,~items, of typeOrder.t. This means theOrdercomponent has a single prop nameditems. - 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.reducerequires the initial value to be passed in. - For each order, we render an
Itemcomponent 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:
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:
items
|> Js.Array.mapi(~f=(item, index) =>
<Item key={"item-" ++ string_of_int(index)} item />
)
|> React.arrayitems
|> Js.Array.mapi(~f=(item, index) =>
<Item key={"item-" ++ string_of_int(index)} item />
)
|> React.arrayThe 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:
{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.elementFile "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.elementThe 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:
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:
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
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:
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:
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
string2 | let answer = value > 100 ? "Too large" : value;
^^^^^
Error: This expression has type int but an expression was expected of type
stringA 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:
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
funsyntax 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
makefunction are treated as props by ReasonReact. - The Js.Array module contains useful array functions
- The
Js.Array.reducefunction is the binding to JavaScript’sArray.reducemethod - The
Js.Array.mapandJs.Array.mapifunctions are both bindings to JavaScript’sArray.mapmethod
- The
- The
React.arrayfunction is needed when you want to convert an array ofReact.elements to a singleReact.element, e.g. after a call toJs.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:
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:
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:
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:
<td> {item |> toPrice |> Format.currency} </td><td> {item |> toPrice |> Format.currency} </td>View source code and demo for this chapter.
Variant types have no equivalent in JavaScript, but they are similar to TypeScript’s union enums ↩︎
Recall that the name of this restaurant is Emoji Cafe, so everything from the menu to the order confirmation must use emojis. ↩︎
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. ↩︎
All lowercase elements like
div,span,table, etc expect their children to be of typeReact.element. But React components (with uppercase names) can take children of any type. ↩︎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. ↩︎