Codementor Events

Getting Started with ReasonML and ReasonReact

Published Oct 18, 2019

In this tutorial, we will go over the structure of a Reason project and take
a look at the example ReasonReact example project provided by Facebook. I will
explain the React code and go into it in further detail in a future tutorial.

Reason, or ReasonML, is a JavaScript-like syntax for OCaml with built-in
support for JSX. Generally it is used in conjunction with the BuckleScript
compiler that compiles OCaml 4.02 code to readable JavaScript without depending
on a separate runtime system. ReasonReact is a great way to write modern
websites. Reason also takes advantage of existing JS ecosystem and uses
npm/yarn to build programs.

For the rest of the tutorial the word Reason will mean a project that uses
ReasonML syntax and the BuckleScript compiler.

Download BuckleScript and ReasonML

I prefer to use yarn, but everything can be done with npm as well.

Init a Reason project with support for React Hooks.

yarn global add bs-platform
bsb -init test-reason -theme react-hooks

Try running the Reason project in a webpack development server.

cd test-reason
yarn
PORT=12345 yarn server

In a browser go to localhost:12345. You should see a simple web app.

Understanding the Structure of a ReasonML Project

You should see the following files in your ReasonML directory. We will
go over them all in detail.

  • .gitignore
  • .merlin
  • bsconfig.json
  • build
  • lib
  • node_modules
  • package.json
  • README.md
  • src
  • webpack.config.js
  • yarn.lock

package.json

If you have done ES6 development before, this file should be familiar to
you. This is the main build file for Reason projects.

  • name: the name of the project.
  • version: the version.
  • scripts: various commands you can perform by typing yarn then the command name.
  • keywords: npm search keywords if you upload this project as a library to npm.
  • author: the name of the author.
  • license: the license you release this project under, list of licenses available.
  • dependencies: libraries that your project directly depends on (from npm, local file system or github).
  • devDependencies: libraries that your project depends on for building and testing (from npm, local file system or github).

When you want to add a package to dependencies, use yarn add <package-name>.
When you want to add a package to devDependencies, use
yarn add <package-name> --dev.

yarn.lock

This file contains a list of the exact version of a library, its location, a
hash of the contents and a list of its dependencies. You should not edit this file
directly. It is created by yarn and should be checked into your versioning
system.

When you run yarn, yarn add or yarn remove, the yarn.lock is updated.
Sometimes you might get version conflicts or multiple versions of a single
package saved to yarn.lock. Usually if I make changes to the dependencies, I
delete yarn.lock, run yarn clean then yarn, make sure all of the
scripts work in package.json, then commit the new yarn.lock.

bsconfig.json

This file is also a build file specific to BuckleScript. Any libraries it depends
on must be in package.json and any libraries you call directly in Reason must
be in bsconfig.json.

  • name: the name of the project. Generally should be the same as the name in package.json.
  • reason: currently the only option is react-jsx. Set it to 3 for react hooks support. Set it to 2 for reason classes only.
  • sources: location of Reason code.
  • package-specs: option to output Commonjs or ES6
  • suffix: suffix of generated file names.
  • namespace: if true then use the directories as part of the namespace, if false then only use the file name. If it is a string then it provides a namespace for all files.
  • bs-dependencies: Reason dependencies used directly in the Reason code.
  • bs-dependencies-dev: Reason dependencies used for building or testing.
  • refmt: The version of reason syntax used: 3 or 2.

webpack.config.js

A simple JavaScript program that packages the compiled result from Reason. It
also provides a small development server.

Here is a simple tutorial on webpack.

src

Where we place our Reason code. This can be changed in sources in
bsconfig.json.

node_modules

The dependency packages downloaded when yarn is run. This is not checked into versioning.

lib

Output from compiling with Reason.

build

Output from running webpack.

README.md

A document with a project introduction. I recommend adding a change list to the bottom
with an entry per version or creating a separate change log file.

.merlin

A file generated by Reason to help IDEs navigate Reason code in the project.

.gitignore

The following files are excluded from versioning.

  • .DS_Store
  • .merlin
  • .bsb.lock
  • npm-debug.log
  • /lib/bs/
  • /node_modules/

Exploring the Reason Code in src

.re files are Reason files. .bs.js are files compiled from Reason to
JavaScript as specified by the suffix in bsconfig.js.

Index.re

This is the main file that gets loaded by index.html. It calls the other
two Reason files in this project.

ReactDOMRe.renderToElementWithId(<Component1 message="Hello! Click this text." />, "index1");

ReactDOMRe.renderToElementWithId(<Component2 greeting="Hello!" />, "index2");

ReactDOMRe is module that comes from the package reason-react.
renderToElementWithId is a function that renders a react component on the
given html id.

If you look in index.html, you can see the two ids that these components are
attached to.

Component1.re

Component1 is a stateless react component. If you have ever used ES6 then the
syntax should be familiar to you. It starts by defining a function to handle a
button click.

let handleClick = (_event) => Js.log("clicked!");
  • let: used when we want to define something.
  • handleClick: the name of the value/function.
  • = (_event) =>: define a function that takes one parameter.
    By placing _ in front of the parameter name we tell the compiler we are
    going to ignore it.
  • Js.log("clicked!");: print "clicked!" to the browser console.

Now we will define the stateless react component.

[@react.component]
let make = (~message) =>
  <div onClick={handleClick}>
    {ReasonReact.string(message)}
  </div>;
  • [@react.component]: a compiler directive we need when defining a React component. The component is named after the file.
  • let make = (~message) =>: a function that the Reason compoment compiler directive expects. ~ means we pass a named parameter.
  • <div onClick={handleClick}>: this is JSX. This defines a div that uses the handleClick function we defined above.
  • {ReasonReact.string(message)}: display the string message parameter in the div.
  • </div>;: close the div and end the function.

Component2.re

Component2 is a stateful component.

Define a record (product) type to represent component's state. The state
maintains to values: an incrementing count and a value that indicates whether
or not to show a message.

type state = {
  count: int,
  show: bool,
};

Define an enum (sum) type to represent the available actions for the component.
This component supports Click and Toggle.

type action =
  | Click
  | Toggle;

Here is the main body of the component. It also starts with the React component
compiler directive and make.

[@react.component]
let make = (~greeting) => {
  let (state, dispatch) = React.useReducer((state, action) =>
  switch (action) {
  | Click => {...state, count: state.count + 1}
  | Toggle => {...state, show: ! state.show}
  }, {count: 0, show: true});

  let message =
    "You've clicked this " ++ string_of_int(state.count) ++ " times(s)";
  <div>
    <button onClick={_event => dispatch(Click)}>
      {ReasonReact.string(message)}
    </button>
    <button onClick={_event => dispatch(Toggle)}>
      {ReasonReact.string("Toggle greeting")}
    </button>
    {state.show ? ReasonReact.string(greeting) : ReasonReact.null}
  </div>;
};

The most important part is let (state, dispatch). React.useReducer is a
function that takes a function to define the action behavior and an initial
state. React.useReducer returns a value state and a function dispatch.

The function passed to React.useReducer takes
two values: state of type state and action of type action. Then it matches
on all possible actions and returns a new state.

When action is Click it increments the value of count by one.
Whenaction is Toggle it alters the show state.

Finally, React.useReducer takes the initial state, {count: 0, show: true}.
These are the values of the component when it is first rendered.

After the state and dispatch definitions, message is defined as
"You've clicked this " ++ string_of_int(state.count) ++ " times(s)";.
The most interesting part is that state.count uses the count value from the
component's state. Every time state.count changes, message gets
updated.

In the JSX body starting with <div> we can see the use of the dispatch function.
When <button onClick={_event => dispatch(Click)}> is clicked, it emits a
Click action and the reducer logic is run. When
<button onClick={_event => dispatch(Toggle)}> is clicked, it emits a Toggle
action to the reducer.

The last part of the JSX body uses state.show. Every time show gets updated,
{state.show ? ReasonReact.string(greeting) : ReasonReact.null} will update
as well. When show is true it renders greeting. When it is false it renders nothing.

Conclusion

I hope that has piqued your interest in Reason and ReasonReact. It should be enough
to help you start exploring the official Reason documents. In the next tutorials,
I will discuss Reason and ReasonReact in greater depth and we will make our own
project.

Resources

Discover and read more posts from James Haver
get started