Mastodon hachyterm.io

UPDATE:
ReasonML + BuckleScript is now Rescript.
As the ecosystem has changed around those tools, this blog post is not accurate anymore.


What Are We Building?

This tutorial will show you how to build a music player with ReasonReact and the useContext hook.
You will learn how to bootstrap a ReasonReact project, how to build a simple application, and how to use hooks with ReasonReact.

☞ View Demo

Why ReasonML and ReasonReact?
ReasonReact provides a type-safe way to build React applications. It leverages the mature OCaml language that comes with a superb type system. It also offers a blazingly fast compiler and excellent JavaScript-oriented tooling.

Disclaimer: I’m a ReasonReact beginner myself, and I can’t promise you that the tutorial will follow best practices.
The blog post series is a port from the excellent How to Use the useContext Hook in React article by James King.

The tutorial assumes a basic familiarity with React. ReasonReact is React under the hood.

You can find all the code on GitHub.

What is useContext?

From the React docs:

Context is designed to share data that can be considered “global” for a tree of React components, such as the current authenticated user, theme, or preferred language.

We will build a custom MusicPlayer component and a useMusicPlayer hook that will allow our app to gain access to a simple mp3 HTML Audio player.

Bootstrapping The Project

ReasonML is a syntax and toolchain for Ocaml, and we will need to install some npm packages to use it. You will need Node.js and npm (or yarn).

Installation

Install Bucklescript and Reason:

npm install -g bs-platform --unsafe-perm

(I use pnpm for installing local packages, that’s why you’ll find a pnpm-lock.yml file in the repository.)

We need BuckleScript to compile ReasonML to JavaScript.

For editor support, check the Reason Docs.
For Vim, you can check my blog post about ReasonML Development With Vim.

Creating The ReasonReact Project

In your terminal:

bsb -init reason-music-player -theme react-hooks
cd reason-music-player && npm install

The commands create a boilerplate project which we’ll have to configure to our needs.

Please note:
The boilerplate has since changed. I’ll help you set up your project with webpack. That isn’t strictly necessary anymore with the new configuration.
If you don’t want to use webpack, you can use the new default boilerplate, which is explained in the README.

First, bsconfig.json. The file tells BuckleScript how to handle the compilation to JavaScript:

{
  "name": "reason-music-player",
  "reason": {
    "react-jsx": 3
  },
  "sources": {
    "dir": "src",
    "subdirs": true
  },
  "package-specs": [
    {
      "module": "es6", // *new*: use ES6 imports
      "in-source": true
    }
  ],
  "suffix": ".bs.js",
  "namespace": true,
  "bs-dependencies": ["reason-react"],
  "refmt": 3
}

Our package.json looks familiar to JavaScript developers. We’ll install concurrently to run both webpack and Bucklescript together:

npm i --save-dev concurrently

Adjust the script section in package.json:

scripts": {
    "start": "concurrently -k \"npm run start:bsb\" \"npm run start:webpack\"",
    "start:bsb": "bsb -clean-world -make-world -w",
    "start:webpack": "webpack-dev-server --port 3000",
    "build": "npm run build:webpack",
    "build:webpack": "NODE_ENV=production webpack",
    "format": "refmt src/*.re"
  },

We’ll also need a configuration file for webpack:

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const outputDir = path.join(__dirname, 'build/')

const isProd = process.env.NODE_ENV === 'production'

module.exports = {
  entry: './src/Index.bs.js',
  mode: isProd ? 'production' : 'development',
  output: {
    path: outputDir,
    filename: 'Index.js',
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      favicon: './src/favicon.ico',
      inject: false,
    }),
  ],
  devServer: {
    compress: true,
    contentBase: outputDir,
    port: process.env.PORT || 8000,
    historyApiFallback: true,
  },
}

Now we can run the project in the terminal and see it on http://localhost:3000:

npm run start

The example project comes with two components, which we’ll delete. Only keep src/Index.re.

Open the index.html file and replace the content:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css"
    />
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.10.2/css/fontawesome.min.css"
    />
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.10.2/css/solid.min.css"
    />
    <title>Reason Music Player</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="Index.js"></script>
  </body>
</html>

Now we are also able to use Bulma CSS and Font Awesome Icons.

We’ll also modify src/Index.re:

ReactDOMRe.renderToElementWithId(<App />, "root");

Our index component will now render the App component into the HTML div with id “root”. We’ll see an error because the App component doesn’t exist yet.

Let’s create it:
src/App.re


[@react.component]
let make = () => <div />

The syntax looks a bit strange. Every ReasonReact component has a make function, which creates a React element.
ReasonReact uses functional components and React hooks under the hood.
Check the ReasonReact documentation for further details.

We’ll create a helper function to render strings in JSX:
src/ReactUtils.re

external s: string => React.element = "%identity";

(Check Florian Hammerschmidt’s post on dev.to for further information.)

Now, let’s update our App component:

open ReactUtils; // (A)

[@react.component]
let make = () =>
  <div className="section is-fullheignt">
    <div className="container">
      <div className="column is-6 is-offset-4">
        <h1 className="is-size-2 has-text-centered">
          {s("Reason Music Player")}    // (A)
        </h1>
        <br />
      </div>
    </div>
  </div>;

We import the utility module, and now we can render a string (see A). Reason is type-safe, so you’ll have to tell the program that you want to pass a string inside JSX.
The classNames are Bulma’s way of styling.

Recap

We installed Reason and BuckleScript and set up the bare minimum for our project.

ReasonReact and BuckleScript offer a familiar tooling experience for JavaScript developers. But we’ve already seen some differences between ReasonML and JavaScript (syntax, being explicit about rendering strings).

Further Reading