Reasonably Reactive Front-ends with ReasonML: Getting Started
There have been several attempts to create front-end alternatives that compile to JavaScript. I've tried most of them to varying degrees:
- I loved ClojureScript but the Interop story was complex.
- Elm killed all my runtime errors but it was a pain having to hack things out or call out to JS when it wouldn't fit what I was trying to do.
- TypeScript added a nice amount of type safety but it was slow and "boilerplate-y".
Reason, though, has the best shot at becoming a first-class citizen in my front-end utility belt!
- It brings in algebraic types, variants, enums, and type checking that I loved in Elm.
- It lets me use mutation where I need it in a more or less straightforward way.
- React integration is a first-class consideration (it's by the guy that created React).
- It compiles FAST (like, in milliseconds)!
- Error checking rivals Elm; it's seriouly good and the error messages are pretty straightforward.
- I can easily call out to JS code. This was my biggest gripe with Elm. With Reason, I feel like the Typechecker and Reason ecosystem helps me avoid runtime errors but gives me a way out when I need to use JS directly.
In this article, I hope to whet your appetite by having you create a basic Reason app and add a basic counter.
First install the Bucklescript platform:
$> npm install -g bs-platform
$> npx create-react-app reasonableTodo --scripts-version reason-scripts
$> cd reasonableTodo
$> yarn start
Now, navigate to https://localhost:3000
:
Now that we have a starter to build off of, let's look at what's actually here. Anyone familiar with React-scripts will recognize the layout. The biggest differences will be in /src
. Here, there are two main files which you should concern yourself with:
index.re
: This is where the dom is mounted.App.re
: The root component.
App.re
is where the root of our React tree resides:
[%bs.raw {|require('./App.css')|}];
[@bs.module] external logo : string = "./logo.svg";
let component = ReasonReact.statelessComponent("App");
let make = (~message, _children) => {
...component,
render: _self =>
<div className="App">
<div className="App-header">
<img src=logo className="App-logo" alt="logo" />
<h2> (ReasonReact.string(message)) </h2>
</div>
<p className="App-intro">
<Counter />
(ReasonReact.string("To get started, edit"))
<code> (ReasonReact.string(" src/App.re ")) </code>
(ReasonReact.string("and save to reload."))
</p>
</div>,
};
Let's dissect this line by line:
[%bs.raw {|require('./App.css')|}];
Modern JavaScript with a typical webpack flow is a labyrinthian mess of standards that would be impossible to support completely. Given the amount of transpilers that may be required in a typical build, it would be impossible for reason to account for everything.
For just such occasions, Reason provides an escape hatch via %bs.raw {| |}. Anything inside the curly braces will be preserved as raw JavaScript and passed to the Babel transpiler. In this example, Babel has a CSS module includer that you can use though the Interop. Reason steps out of your way when you need to do something nonstandard.
Reason provides a
Js
interface for accessing the browserApi's though a functional wrapper. For instance, console.log can be accessed as Js.log().
[@bs.module] external logo : string = "./logo.svg";
This part is new to me, but essentially it declares 'logo' as a submodule which it then loads as a string. when I Js.log() it, 'logo' gets printed out as "/static/media/logo.e2342b05.svg"
. In other words, it dynamically generates the build url link for the svg icon.
let component = ReasonReact.statelessComponent("App");
Notice that we pass in "App" as the name. Reason binds it so that it can be used anywhere similarly to how you can globally include angular directives. It's why you can directly use <App message="foobar"/> in
index.re`.
Unlike React in JS where we extend React.Component, in ReasonML, you create a component as the base from which we derive your specific component. In some ways, it's very similar to extending the prototype manually before ES6 classes became widespread.
//no one writes js like this anymore.
var baseObj = {.._methods_go_here_..};
SuperFunc.prototype = Object.extend({}, baseObj);
let instance = new SuperFunc();
In this particular case, we are creating a Stateless Component as opposed to a stateful component which we will see later on.
let make = (~message, _children) => {
...component
...
};
Reason goes through the effort of making the props super-duper explicit. make is expected by the Reason system as a function for generating the component. The parameters that are passed in are the props, and children are always last. Unlike Javascript, arity is super important and cannot be ignored. If you don't use children, add a _ to the end which prevents the compiler from issuing warnings on unused variables. In this case, ~message is the first paramater, which is also a React prop and would be props.message
in a standard React component.
Props can also be given nullable and default values; we could just as easily have given it ~message=?
for an optional message or ~message="hello world"
to give it a default value.
...component
Similar to ES6 spread operators, this syntax here makes our component inherit behavior from the base stateless component.
...component,
render: _self =>
<div className="App">
<div className="App-header">
<img src=logo className="App-logo" alt="logo" />
<h2> (ReasonReact.string(message)) </h2>
</div>
<p className="App-intro">
(ReasonReact.string("To get started, edit"))
<code> (ReasonReact.string(" src/App.re ")) </code>
(ReasonReact.string("and save to reload."))
</p>
</div>,
Unlike the JavaScript version of React, there is no this
, so for lifecycle values, Reason provides a self which hooks into the React component methods. Beyond that, this should be straightforward aside from the need to use ReasonReact.string. It exists to enable the bulletproof typechecking that makes it so great.
Now that we've covered the basics, lets add a small counter to the page. In a traditional React App, we might use a redux store:
const ADD_ACTION = 'ADD_ACTION';
const addAction = () => ({ type: ADD_ACTION })
const reducer = (state=0, action) => {
switch (action.type) {
case ADD_ACTION:
state = state + 1;
return state;
default:
return state;
}
}
Reason itself completely makes obsolete the use of Redux by having a built-in stateful component that wraps a much cleaner first class reducer system as a first class citizen. To create a stateful component, we use ReasonReact.reducerComponent(_name_)
.
There are two new paramaters that need to be added to the make function:
- initialState: A function that returns the initial state, similar to how it works in Redux.
- reducer: Just like a Redux reducer that takes a state and an action; the reducer returns a new state object.
Last but not least, we need to create some actions. Here's where Reason really shines. Redux forces us to rely on comparatively primitive tools such as string comparison. Reason brings the full power of variants from its OCaml roots. Basically, we can Typecheck our actions at compile time!
Defining Add and Subtract becomes much simpler and straightforward. We just declare them as variants of an action type.
/* Counter.re */
type action =
| Add
| Subtract;
Creating the component is similar to creating a stateless component plus the two aforementioned parameters. Our reducer uses a switch statement similarly to Redux, only we use ReasonReact.Update() to update the state. This is primarily for supporting asynchronous behavior:
/* Counter.re */
let component = ReasonReact.reducerComponent("Counter");
let make = (_children) => {
...component,
initialState: () => 0, /* sets the initial state to 0 */
reducer: (action, state) =>
switch action {
| Add => ReasonReact.Update(state + 1)
| Subtract => ReasonReact.Update(state - 1)
},
render: (_self) => {
let countMessage = "Count: " ++ string_of_int(self.state);
<div>
<p>(ReasonReact.stringToElement(countMessage))</p>
</div>
},
};
The last thing we need here are buttons for actually dispatching these actions. Thats where _self comes in. The Render function is passed a self argument which gives it access to the component helpers. In this case, we can use self.send to dispatch an action. Used with callbacks in buttons, we can complete our Counter component:
/* Counter.re */
type action =
| Add
| Subtract;
let component = ReasonReact.reducerComponent("Counter");
let make = (_children) => {
...component,
initialState: () => 0,
reducer: (action, state) =>
switch action {
| Add => ReasonReact.Update(state + 1)
| Subtract => ReasonReact.Update(state - 1)
},
render: (self) => {
let countMessage = "Count: " ++ string_of_int(self.state);
<div>
<p>(ReasonReact.stringToElement(countMessage))</p>
<div>
<button onClick=(_event => self.send(Add))>
(ReasonReact.stringToElement("++"))
</button>
<button onClick=(_event => self.send(Subtract))>
(ReasonReact.stringToElement("--"))
</button>
</div>
</div>
},
};
To add it to our app, simply add <Counter /> into the App.re:
[%bs.raw {|require('./App.css')|}];
[@bs.module] external logo : string = "./logo.svg";
Js.log(logo);
let component = ReasonReact.statelessComponent("App");
let make = (~message, _children) => {
...component,
render: _self =>
<div className="App">
<div className="App-header">
<img src=logo className="App-logo" alt="logo" />
<h2> (ReasonReact.string(message)) </h2>
</div>
<p className="App-intro">
<Counter />
(ReasonReact.string("To get started, edit"))
<code> (ReasonReact.string(" src/App.re ")) </code>
(ReasonReact.string("and save to reload."))
</p>
</div>,
};
With that, I hope you tune in when I expand on this in future articles as I build something more substantive!