Side Project - ReasonReact Puzzle

A few weeks ago I learned about ReasonReact. It's the React library written with Reason, an Ocaml/Javascript language made by Facebook.

In this blog post I will explain what I learned while coding a small sliding puzzle game in ReasonReact.

Reason is a new syntax based on Ocaml and Javascript. It compiles into Javascript, Ocaml or even Assembly.
The last time I used Ocaml was when I was a student so I knew I would struggle with it a bit but I also knew that I might really like some patterns that I would use.

Why try ReasonReact?

I don't have a lot of experience with Ocaml but I remember liking functional programming. When reading the doc of ReasonReact I found that it could actually solve or leverage some design I struggle with in my other projects.
This is why I wanted to give it a try: to see how the syntax works and to see how I could solve some of my design issues.

The project

When I'm testing new languages or new patterns, I like to code a small simple project to have a real life test. I also like implementing small games.

This time I decided to implement a Sliding Puzzle Game.

I wanted to keep things simple:

  • A simple web page
  • A fixed list of puzzles
  • A fixed range of difficulties

You can find the source code here: ArmandDu/slidingPuzzleReasonReact and try it live here: https://puzzle.apptize.fr

Lessons Learned

Starting with a new language with only the doc can lead to long time figuring out how to find the solution when you get stuck.
Thankfully, there is a really active community there to help. It is really useful.


bsconfig.json

One good thing with ReasonReact is that there is no endless configuration with webpack or a new build tool.
I decided to use Create React App with the Reason-scripts to setup the app. Everything was almost perfectly configured just with that. There is a regular package.json and a new Bucklescript .bsconfig file.

Because I like to nest folders in my source tree, the only thing I needed to change from my config files was to change the sources entry to allow subdirs.
This was the changed introduced in the bsconfig.json:

- "sources": "src",
+ "sources": [
+    { "dir": "src", "subdirs": true }
+  ],

pattern matching feature

Pattern Matching is actually the thing I remember the most from Ocaml.

Pattern Matching is when I use the switch operator or the ImmutableJs's Map object in Javascript. But Reason's and Ocaml's is way more powerful and useful.

This is what I could write in Javascript:

import {Map} from 'immutable';

const ConditionalComponent = ({value, ...props})  => Map()
    .set('small', <CaptionPost {...props} />)
    .set('full', <ExpandedPost {....props} />
    .get(value, null)

//OR

switch (action) {
    case 'EDIT_POST': ...
    case 'DELETE_POST': ...
    default: ...
}

It works fine but there are some limitations.
For example Those won't work:

switch (array) {
    case ["/"] : ....
}
//or
switch (value) {
    case _ > 0 : ....
}

This is where the pattern matching beats everything since it can match any values or match multiple values (using a tuple) or match Variants and also match conditions with the use of when.

Here is what it looks like to create a function that maps a Variant to a tuple:

    let mapDifficulty = difficulty => switch(difficulty) {
        | Difficulty.Easy => (3, 3)
        | Difficulty.Medium => (5, 5)
        | Difficulty.Hard => (9, 9)
        | Difficulty.Expert => (12, 12)
    };

This is an example of conditional rendering:

switch (image: (int, string), difficulty: (int, int) ) {
     | (Some((_, url)), (x, y)) => ( <Game x=x y=y url=url ... /> )
     | _ => ( <Menu ... /> )
  }

Which would be equivalent to:

  if (image !== null) {
      const { url } = image;
      const { x, y } = difficulty;
      return <Game ... />
   }
   else {
       return <Menu .../>
   }

In Reason, Pattern Matching is present everywhere. It replaces the if statement and forces you to match all use cases


passing props

In ReasonReact we need to pass the props that is asked by the component, except if this prop is optional(nullable) or has a default value.

To ask for a prop, we need to set a named argument it in the component's make function:

let component = statelessComponent("Foo");
let make = (~myProp: string) => {};

and then when using the component:

<Foo myProp="some string" />

Reason is a strongly typed syntax. this forces us to do two things:

  • either pass a valid non null value to our props
  • or we will be asked to test it before using it

For instance an optional int will have a different type than an int. So doing optionalInt + 10 won't work. We will need to check that there is a value inside the variable. For instance:

let computedValue = switch(optionalInt) {
    | Some(value) => value + 10
    | None => 0
}

This is really powerful since we know that we cannot have invalid or unset props.


stateless vs "stateful" components

Because Reason is strongly typed, we need to explicitly tell if our component will be "stateless" or "stateful".

In ReasonReact we achieve this by creating a ReasonReact.statelessComponent or a ReasonReact.reducerComponent.

In a statelessComponent, There is no state management, we simply render a component along with its props and children.

In a reducerComponent we need to define a state type and an initialState. we also have to implement a reducer function.

We call the reducer function by sending an action from our component using self.send(action) and the reducer function will have to tell if an update is required or not.

For instance take src/ui/Game/newgame.re We define our actions:

type action =
| SelectImage(image)
| SelectNone
| SetDifficulty(Difficulty.difficulty);

Then we initialize the state:

initialState: () => {difficulty: Difficulty.Medium, image: None },

And finally, we implement our reducer function:

reducer: (action, state) => {
            switch (action) {
                | SelectImage(image) => ReasonReact.Update({...state, image: Some(image)})
                | SelectNone => ReasonReact.Update({...state, image: None})
                | SetDifficulty(difficulty) => ReasonReact.Update({...state, difficulty})
            };
        },

And that's it. We have an exhaustive state management and everything is done in one place. We don't have a dozen functions dispatched in our file that will call setState. There is only one function. We can then call the self.send method safely.

This is probably the best new design I learned and loved. I even forked this project and did it in React to implement a reducerComponent feature. See the repo here: ArmandDu/slidingPuzzleReact.


Routing

ReasonReact is shipped with a really simple Router.

Once combined with the subscription helper and the pattern matching, the routing gets easy to implement.

I haven't refactored my code in the repo but here how it works
First, we watch the url change and just pattern match it:

subscriptions: (self) => [
            Sub( 
                () => ReasonReact.Router.watchUrl(url =>
                           switch(url.path) {
                               | ["/"] | [] => self.send(GoTo(Home))
                               | ["about"] => self.send(GoTo(About))
                               | _ => self.send(GoTo(NotFound))
                           };
                      ),
               ReasonReact.Router.unwatchUrl
            )
        ],

We need to store the change into the component's state:

reducer: (action, _state) => {
        switch (action) {
            | GoTo(route) => ReasonReact.Update({route})
        }
    },

And finally render the correct Component:

    render: self => {
        ...
        (
            switch (self.state.route) {
                | Home => <Home />
                | About => <About />
                | NotFound => <NotFound />
            }
        )
        ...
    }

Conclusion

ReasonReact can be quite a pain in the ass since it will tell us when we did something wrong. It can also slow down productivity at first but then when things are working they are working nice and fast. The syntax also removes a whole bunch of flaws and forbids us to make some mistakes. I think at the end of the day we are actually more productive because the code we leave behind is most likely to just work.

I really like ReasonReact and Reason but I'm not sure I will use it a lot with for work or long term projects. I might still do some side projects with it though. I will see what the future holds.


This article is a first draft. I might edit it in the future.