Let’s build it!

We’ll start from Create React App, and build this project together.

First, we’ll build a basic App Shell that doesn’t respond to touch, and then we’ll work on the touch features. The touch features include:

when open, the navigation drawer should respond to TouchMove events (as shown in the demo)

if the user moves the navigation drawer after the 50% threshold (half closed), the drawers should close — otherwise, remain open

if the user swipes with some minimum velocity, the drawer should close — even if the 50% threshold hasn’t been reached

The features we’ll work on now include:

animating the navigation drawer

animating the overlay that hides main content when the navigation drawer is open

preventing body scrolling when the navigation drawer is open

closing the navigation drawer when the route has changed

closing the navigation drawer on outside clicks

Note: If you haven’t already, install reason-cli & set up your editor. Additionally, install BuckleScript via npm install -g bs-platform .

Initialize your developer environment with Create React App

yarn create react-app my-app --scripts-version reason-scripts

Remove <App />’s message prop in index.re

On line 5, <App message=”…” /> becomes <App />

Replace app.re with the following:

This is now a stateful component that just renders: <div class=”App” />

Include app.scss here (the repo is already configured to use SASS).

Include index.css here.

Run npm run start and we should see a blank page. Great! Let’s review what’s going on in app.re .

Before going further, make sure you’ve read the stateful component docs.

There are currently two possible actions:

Navigate(routeWithTitle) ToggleMenu(bool)

And, in state, we have two things we care about:

type routeWithTitle = (route, string); type nav = {isOpen: bool}; ... type state = {

...,

routeWithTitle,

nav

};

First, routeWithTitle contains the current route and title of the page. Second, nav contains the state of the navigation drawer.

Our reducer and render fields are currently mostly placeholder. Let’s address these now.

This looks like a lot, but that’s probably because I’ve inlined SVGs. Let’s focus on the highlights:

let (route, title) = self.state.routeWithTitle;

Remember how routeWithTitle was a tuple? We can destruct the current route and title just like that.

className=("App" ++ (self.state.nav.isOpen ? " overlay" : ""))

Just like in ReactJS, we can conditionally add CSS classes based on state. In this case, app.re renders either:

<div class=”App”>

or

<div class=”App overlay”>

and the corresponding CSS:

So, when the navigation drawer is open, <App /> gets the overlay class, which causes an overlay to fade in over the contents of <App/> .

Note: With the overlay class, transform is not transitioned on .App:after . But, without the overlay class, it is — with a delay. This is because we want the overlay to have transform: translateX(0%) right until the navigation drawer is closed. Otherwise, the overlay will suddenly disappear instead of nicely fading out.

The <nav /> element gets the active class when the navigation drawer should be open.

<nav className=(self.state.nav.isOpen ? "active" : "")

In the header, clicking on an anchor tag dispatches an action:

<header>

<a

onClick=(

event => {

ReactEventRe.Mouse.stopPropagation(event);

self.send(ToggleMenu(! self.state.nav.isOpen));

}

)>

The action is ToggleMenu(! self.state.nav.isOpen). What happens next? Just like in Redux, a reducer handles all state changes. You send an action to the reducer, and then, based on that action, state changes.

What are our available actions? They’re defined in our action type.

type action =

| Navigate(routeWithTitle)

| ToggleMenu(bool);

In the render function, we self.send(...) an action, which invokes our reducer:

reducer: (action, state) =>

switch action {

| Navigate(_routeWithTitle) => ReasonReact.NoUpdate

| ToggleMenu(isOpen) =>

ReasonReact.Update({

...state,

nav: {

isOpen: isOpen

}

})

},

If the action is ToggleMenu, we’ll set state.nav.isOpen to ToggleMenu’s argument.

But — we can do better! Let’s prevent body scrolling while the navigation drawer is open.

In normal HTML5 world, that would mean setting overflow: hidden on the <html> element. But we can’t do that in Reason, right?

We can.

ReasonML is amazing. Why? Because although it’s suited for complex apps, it doesn’t get in your way when you want to build simple stuff.

Let’s look at two ways we can solve this problem.

Solution 1: Raw JavaScript

Instead of ReasonReact.Update, we’ll use ReasonReact.UpdateWithSideEffects, which takes a callback to do side effects. Learn more about that here.

In the callback, we’ll set the style of the <html> element based on the state of the nav.

What? Did we just write raw JavaScript inside of Reason? Yes.

Solution 2: Type-safe

When running raw JavaScript, we throw type safety out the window. Sometimes that’s fine, and sometimes that’s less than ideal. Reason let’s you decide.

Also, sometimes it’s nice to just work in JavaScript and figure out how to convert to a type-safe method later.

Here, we bind to the documentElement via BuckleScript’s external declaration. In the reducer, we set the style of the documentElement using Reason’s Object syntax.

Note: Reason/OCaml has excellent interoperability with JavaScript through BuckleScript. Learn more about that here.

Alright, let’s implement the reducer’s Navigate case.

| Navigate(routeWithTitle) =>

ReasonReact.UpdateWithSideEffects(

{...state, routeWithTitle},

(self => self.send(ToggleMenu(false)))

)

We update state with the current route and page title, and again call a side effect, where we dispatch another action to close the navigation.

We could have also updated the nav state directly like so:

| Navigate(routeWithTitle) =>

ReasonReact.Update({

...state,

routeWithTitle,

nav: {

isOpen: false

}

})

And, since we are changing the current route in application state, we can conditionally render components based on that state.

<main>

(

switch route {

| Home => <Home />

| Page1 => <Page1 />

| Page2 => <Page2 />

| Page3 => <Page3 />

}

)

</main>

Just make sure you have defined components in home.re , page1.re , page2.re and page3.re .

Here’s a simple stateless <Page1 /> component:

Congrats! We now have a simple App Shell with a navigation drawer and a router. The App Shell renders a “main” component based on the current route. Additionally, if you want to e.g. render more than one component per route, you can use whatever logic you’d like in your render function.

Note: Since we are using a tuple for routeWithTitle, we are guaranteed that our route and page title will never get out of sync (unless, of course, we simply include the wrong title when dispatching self.send(Navigate((Home, "Wrong Title"))) . But self.send(Navigate(Home)) will fail at compile time — since Navigate only accepts an argument of type routeWithTitle.

Whenever possible, use the type system in your favor to provide you with stronger guarantees. For more on this, please check out the following talks: