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:
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
Hotdog
constructor toItem.t
- Add a
| Hotdog
branch 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
Order
module isarray(Item.t)
, which is an array of variants. - The
Order.make
function has a single labeled argument,~items
, of typeOrder.t
. This means theOrder
component 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.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
:
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 Item
s
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.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
:
{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:
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
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
:
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’sArray.reduce
method - The
Js.Array.map
andJs.Array.mapi
functions are both bindings to JavaScript’sArray.map
method
- The
- The
React.array
function is needed when you want to convert an array ofReact.element
s 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. ↩︎