Real-Time Communication in ReasonML with bs-socket
Tags: reason, react, beginners, tutorial
Published: 2019-03-03 22:19:43

---
title: Real-Time Communication in ReasonML with bs-socket
published: true
description: A walkthrough of a demo client and server using bs-0ocket
tags: reasonml, react, beginners, tutorial
cover_image: https://cdn-images-1.medium.com/max/2000/1*theA-BiTPVjhIlQ4njM9VQ.jpeg
---

In this post I'll demonstrate some real-time communication in a simple application using [ReasonML](https://reasonml.github.io/).  If you're brand new to Reason, some assumed basic comfort in JavaScript should be most of what you need, and there's a handy [cheatsheet](https://reasonml.github.io/docs/en/syntax-cheatsheet) to get you started.

I'm using the [bs-socket](https://github.com/reasonml-community/bs-socket.io) bindings for [socket.io](https://socket.io/), a widely used Node.js real-time engine, and their [example](https://github.com/reasonml-community/bs-socket.io/tree/master/example) as a base.

The finished application will present each client with a set of named buttons and a dialog box to add a new button, as well as a running total of connected clients.  Clicking a button will remove it from the set, and this set will stay in sync across all connected clients.

## Requirements

This is a [Node](https://nodejs.org/en/) project.  I'll be using [yarn](https://yarnpkg.com/en/) if you'd like to follow along exactly.  All other dependencies will be handled by node.

## Setup

First install the [BuckleScript](https://bucklescript.github.io/) platform if you do not already have it have it:

```
$ yarn global add bs-platform
```

Now we can use the `bsb` build tool to create a basic project:

```
$ bsb -init reason-buttons -theme basic-reason
$ cd reason-buttons/
$ yarn start
```

This will start the compiler in watch mode - any changes you make to a file will immediately trigger a recompile of the resulting JavaScript, right next to the source.  Verify you see both `Demo.re` and `Demo.bs.js`. under `reason-buttons/src`.  Rename your Reason file to `ButtonServer.re` and see it immediately recompile to reflect the difference - `Demo.bs.js` is removed and the same contents now fill `ButtonServer.bs.js`.

Add a script to your newly generated `package.json` to execute this file:

```json
// ..
"scripts": {
  "build": "bsb -make-world",
  "serve": "node src/ButtonServer.bs.js",  // <- here
  "start:re": "bsb -make-world -w",
  "clean": "bsb -clean-world"
},
// ..
```

I also renamed `start` to `start:re` - feel free to manage your scripts however is most comfortable.

One change I always immediately make in a Node.js app is pulling the port number out so it can be specified via environment variable.  Luckily, interop is dirt simple!  We can just use Node to grab it from an environment variable.  Create a file at `src/Extern.re` with the following contents:

```ocaml
[@bs.val] external portEnv: option(string) = "process.env.PORT";
[@bs.val] external parseInt: (string, int) => int = "parseInt";
```

The `[@bs.val]` syntax is a BuckleScript compiler directive.  There's an overview of the various syntaxes [here](https://bucklescript.github.io/docs/en/interop-cheatsheet) and the rest of that guide goes in depth about when to use each.  I won't get too far into the nuts and bolts of JS interop in this post, the docs are thorough and for the most part in find the resulting code legible.  The basic idea is that the keyword `external` is kind of like `let` except the body is a string name pointing to the external function.  This way we can incrementally strongly type the JavaScript we need and have Reason typecheck everything smoothly.

This code will also leverage the `option` [data type utilities](https://bucklescript.github.io/bucklescript/api/Belt.Option.html) for nullable values like `getWithDefault` from [`Belt`](https://bucklescript.github.io/bucklescript/api/Belt.html), the standard library that ships with Reason.  Replace the contents of `src/ButtonServer.js` with the following:

```ocaml
open Belt.Option;
open Extern;

let port = getWithDefault(portEnv, "3000");

print_endline("Listening at *:" ++ port);
```

I like to use `3000` for my default, you're of course welcome to use whatever you like.

Over in `ButtonServer.bs.js` the compiled output is quite readable:

```javascript
// Generated by BUCKLESCRIPT VERSION 4.0.18, PLEASE EDIT WITH CARE
'use strict';

var Belt_Option = require("bs-platform/lib/js/belt_Option.js");
var Caml_option = require("bs-platform/lib/js/caml_option.js");

var port = Belt_Option.getWithDefault((process.env.PORT == null) ? undefined : Caml_option.some(process.env.PORT), "3000");

console.log("Listening at *:" + port);

exports.port = port;
/* port Not a pure module */
```

Lets verify it works.  Open up a separate terminal and type `yarn serve`.  You should see the following:

```
$ yarn serve
yarn run v1.13.0
$ node src/ButtonServer.bs.js
Listening at *:3000
Done in 0.09s
$
```

## Dependencies

For an example of how to use node's `Http` module manually see [this post](https://notes.maciejsmolinski.com/2018/03/02/reasonml-http-server-in-node-js/) by Maciej Smolinski.  For simplicity's sake I'll just use the community bindings for [`bs-express`](https://github.com/reasonml-community/bs-express).  We'll also pull in `bs-socket`:

```
$ yarn add -D bs-express https://github.com/reasonml-community/bs-socket.io.git
```

Then add it to `bs-config.json`:

```json
// ..
"bs-dependencies": [
  "bs-express",
  "bs-socket"
],
// ..
```

Bucklescript will take care of the rest as long as the package in question has a `bsconfig.json`.

## Messages

Before we actually implement our server, though, we need to define some message types.  This will help us plan the scope of the application.  Create a new file at `src/Messages.re` with the following contents:

```ocaml
/* Messages */

type labelName = string;
type buttonList = list(labelName);
type numClients = int;

type msg =
  | AddButton(labelName)
  | RemoveButton(labelName);

type clientToServer =
  | Msg(msg)
  | Howdy;

type serverToClient =
  | Msg(msg)
  | ClientDelta(int)
  | Success((numClients, buttonList));
```

These are the various messages we'll be sending back and forth.  This is the biggest difference from using `socket.io` in JavaScript, where custom events are named with strings.  Here we always just emit a generic message but use ReasonML pattern matching to destructure the payload itself.  The library currently doesn't cover stringly typed events, though the one issue open is [asking about it](https://github.com/reasonml-community/bs-socket.io/issues/9).  The readme on that GitHub repo puts it succinctly:  "The API differs a bit from socket.io's API to be more idiomatic in Reason. Generally, e.g. JavaScript's `socket.emit("bla", 10)` becomes `Server.emit(socket, Bla(10))` in Reason".

Take a look at `Messages.bs.js`:

```js
// Generated by BUCKLESCRIPT VERSION 4.0.18, PLEASE EDIT WITH CARE
/* This output is empty. Its source's type definitions, externals and/or unused code got optimized away. */
```

They don't end up represented at all in our bundle - it's just a compile-time benefit.  Neat!

## The Server

### Express

Alright - one last step before we can write our server.  Back in `src/Extern.re`, add the following typings for `Http` at the bottom of the file:

```ocaml
module Http = {
  type http;
  [@bs.module "http"] external create: Express.App.t => http = "Server";
  [@bs.send] external listen: (http, int, unit => unit) => unit = "";
};
```

Now we're ready!  Get back into `src/ButtonServer.re` and make it look like this:

```ocaml
open Belt.Option;
open Express;
open Extern;

let port = getWithDefault(portEnv, "3000");

let app = express();

let http = Http.create(app);

Http.listen(http, port |> int_of_string, () =>
  print_endline("Listening at *:" ++ port)
);
```

`|>` is the pipe operator.  In brief, `a |> b` is the same as `b(a)`.  It can be much more readable when chaining multiple functions.

Just to verify it works, add a placeholder `/` endpoint, above the `Http.listen()` line.  We'll come back to the client.

```ocaml
App.get(app, ~path="/") @@
Middleware.from((_, _) => Response.sendString("<h1>HELLO, REASON</h1>"));
```

Alright, I lied - there's one more bit o' syntax there.  Per [the docs](https://reasonml.github.io/api/Pervasives.html) `(@@)` is the application operator - "g @@ f @@ x is exactly equivalent to g (f (x))."  If you're familiar with Haskell, it's `($)`, or if you're familiar with...math, I guess, it's `g o f(x)`.

Let's make sure we're good to go:

```
$ yarn serve
$ node src/ButtonServer.bs.js
Listening at *:3000
```

If you point your browser, you should see **HELLO REASON**.

### Socketry

Now for the real-time bits!  Add the following two lines below your `/` endpoint, but above your call to `Http.listen()`:

```ocaml
module Server = BsSocket.Server.Make(Messages);

let io = Server.createWithHttp(http);
```

Now `socket.io` is configured to use the newly defined Message types.  To keep track of the current set of buttons and connected clients, we'll need some state:

```ocaml
type appState = {
  buttons: list(string),
  clients: list(BsSocket.Server.socketT),
};

let state = ref({buttons: ["Click me"], clients: []});
```

The state is held inside a mutable `ref`.  We can access the current contents via `state^`, and assign to it with the assignment operator `:=`.  When the server starts up it has no clients and one default button.

Also handy is this helper function to emit a message to every client stored except the client passed:

```ocaml
let sendToRest = (socket, msg) =>
  state^.clients
  |> List.filter(c => c != socket)
  |> List.iter(c => Server.Socket.emit(c, msg));
```

Now everything is set up to define the real meat of the application.  Start with the following outline:

```ocaml
Server.onConnect(
  io,
  socket => {
    // our code here....
  },
);
```

The first part is how to handle a client connecting.  Replace the placeholder comment with the following:

```ocaml
open Server;
    print_endline("Client connected");
    state := {...state^, clients: List.append(state^.clients, [socket])};
    sendToRest(socket, ClientDelta(1));
    Socket.emit(
      socket,
      Success((List.length(state^.clients), state^.buttons)),
    );
```

For convenience we'll open our `Server` module into the local scope, and then adjust our state to include the new client.  We use the `sendToRest` function to emit the `ClientDelta` message to everyone else who may already be stored in `state.clients`, and finally send back the `Success` message, telling the newly connected client about the current state.

The next order of business is handling the disconnect.  Right below the last `Socket.emit()` call add:

```ocaml
    Socket.onDisconnect(
      socket,
      _ => {
        print_endline("Client disconnected");
        sendToRest(socket, ClientDelta(-1));
        state :=
          {...state^, clients: List.filter(c => c == socket, state^.clients)};
      },
    );
```

The client gets dropped from the app state and everyone else still connected is updated on the change.  The only part left is to handle the `clientToServer` messages we defined in `Messages.re`:

```ocaml
Socket.on(
      socket,
      fun
      | Msg(msg) => {
          switch (msg) {
          | AddButton(name) =>
            print_endline("Add " ++ name);
            state :=
              {...state^, buttons: state^.buttons |> List.append([name])};
            sendToRest(socket, Msg(AddButton(name)));
          | RemoveButton(name) =>
            print_endline("Remove " ++ name);
            state :=
              {
                ...state^,
                buttons: state^.buttons |> List.filter(a => a == name),
              };
            sendToRest(socket, Msg(RemoveButton(name)));
          };
        }
      | Howdy => {
          print_endline("Howdy back, client");
        },
    );
```

Whenever a button is added or removed, we adjust our state accordingly and let everyone else know about the change.  That's it for the server!  

## The Client

### Nuts 'n' Bolts

I'd feel remiss if I didn't use the ReasonReact library for this demo.  It's excellent.  First, add the dependencies:

```
$ yarn add react react-dom
$ yarn add -D reason-react
```

Also add `reason-react` to `bsconfig.json`:

```json
  "bs-dependencies": [
    "bs-express",
    "bs-socket",
    "reason-react"
  ],
```

While we're in here, let's activate JSX.  Add the following entry to the top level:

```json
  "reason": {
    "react-jsx": 2
  },
```

To handle bundling, I'm going to use [Parcel](https://parceljs.org/).  This is not necessary - you're welcome to use anything you're comfortable with.  To follow along, add the dependency:

```
$ yarn add -D parcel-bundler
```

Also add a script to `package.json` to run it:

```json
"scripts": {
  //..
  "start:bundle": "parcel watch index.html",
  //..
},
```

We also need to create that `index.html`.  Put it at your project root:

```html
<!-- https://github.com/sveltejs/template/issues/12 -->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Reason Buttons</title>

    <script id="s"></script>
    <script>
        document.getElementById('s').src = "socket.io/socket.io.js"
    </script>

</head>

<body>
    <div id="app"></div>
    <script defer src="./src/Index.re"></script>
</body>

</html>
```

This stub includes a [workaround](https://github.com/sveltejs/template/issues/12) in the head for using Parcel with socket.io on the client side.  Also note that Parcel understands ReasonML - we can pass in `Index.re` for the entry point directly.  Once this file is here, open a new terminal and enter `yarn start:bundle` - this can be left running and will recompile your bundle when needed.

We now need to tell our server to serve this file instead of our placeholder string.  We'll use a little more interop from this - add the following to `Extern.re`, helpfully lifted from the [bs-socket example](https://github.com/reasonml-community/bs-socket.io/blob/master/example/ExampleServer.re):

```ocaml
module Path = {
  type pathT;
  [@bs.module "path"] [@bs.splice]
  external join : array(string) => string = "";
};

[@bs.val] external __dirname : string = "";
```

Now replace the endpoint in `ButtonServer.re` with:

```ocaml
App.use(
  app,
  {
    let options = Static.defaultOptions();
    Static.make(Path.join([|__dirname, "../dist"|]), options)
    |> Static.asMiddleware;
  },
);

App.get(app, ~path="/") @@
Middleware.from((_, _, res) =>
  res |> Response.sendFile("index.html", {"root": __dirname})
);
```

This sets up our static file serving and serves `dist/index.html`, which is generated by Parcel, at `/` instead of the placeholder string.

### Code

We've pointed Parcel towards `src/Index.re` - might be a good idea to put a file there!  Create it with the following contents:

```ocaml
ReactDOMRe.renderToElementWithId(<ButtonClient />, "app");
```

This is how ReasonReact mounts to the DOM.  We're finally ready to build the component.

In a real app, this would ideally be split into several components - one for the buttons, one for the input, maybe a separate one for the counter.  For demonstration purposes I'm just throwing it all in one component, but if this app were to get much larger splitting it apart would likely be step number one.

Create a file at `src/ButtonClient.re`.  First, we'll set up our socket client at the top of the file:

```ocaml
module Client = BsSocket.Client.Make(Messages);

let socket = Client.create();
```

Below that, we need to define the `state` for our component as well as the `action`s we can take to transform that state in order to create a `reducerComponent`:

```ocaml
type state = {
  numClients: int,
  buttons: list(string),
  newButtonTitle: string,
};

type action =
  | AddButton(string)
  | ClientDelta(int)
  | RemoveButton(string)
  | Success((int, list(string)))
  | UpdateTitle(string);

let component = ReasonReact.reducerComponent("ButtonClient");
```

This is pretty similar to the `socket.io` messages, with the addition of a `newButtonTitle` to allow the client to name the buttons they add.

The rest of the component will live in this skeleton:

```ocaml
let make = _children => {
  ...component,
  initialState: _state => {numClients: 1, buttons: [], newButtonTitle: ""},
  didMount: self => {
    // socket.io message handling
  },
  reducer: (action, state) =>
    switch (action) {
      // actions
    },
  render: self =>
    <div>
      <h1> {ReasonReact.string("Reason Buttons")} </h1>
      <div>
        // Buttons
      </div>
      <div>
        // Add A Button
      </div>
      <span>
        // Current Count
      </span>
    </div>,
};
```

We'll look at each section separately.  The `initialState` given here will just be used to render the component right off the bat - as soon as our client connects, it's going to receive a `Success` message which will overwrite this value.

We need to translate incoming `socket.io` messages.  I've put this in the `didMount` method to make sure our client has successfully loaded.  Replace the placeholder with:

```ocaml
Client.on(socket, m =>
      switch (m) {
      | Msg(msg) =>
        switch (msg) {
        | AddButton(name) => self.send(AddButton(name))
        | RemoveButton(name) => self.send(RemoveButton(name))
        }
      | ClientDelta(amt) => self.send(ClientDelta(amt))
      | Success((numClients, buttons)) =>
        self.send(Success((numClients, buttons)))
      }
    );
    Client.emit(socket, Howdy);
```

The `Client.on()` portion is pattern matching on the incoming `serverToClient` messages and mapping it to the proper ReasonReact `action`.  We also send back a `Howdy` message to the server once successfully loaded.

The next order of business is our reducer.  We need to define how exactly each `action` should manipulate our `state`:

```ocaml
switch (action) {
| AddButton(name) =>
  ReasonReact.Update({
    ...state,
    buttons: List.append(state.buttons, [name]),
  })
| ClientDelta(amt) =>
  ReasonReact.Update({...state, numClients: state.numClients + amt})
| RemoveButton(name) =>
  ReasonReact.Update({
    ...state,
    buttons: List.filter(b => b != name, state.buttons),
  })
| Success((numClients, buttons)) =>
  ReasonReact.Update({...state, numClients, buttons})
| UpdateTitle(newButtonTitle) =>
  ReasonReact.Update({...state, newButtonTitle})
},
```

The `...` spread operator is a huge help!  This code also takes advantage of a feature called "punning" - for instance, in `UpdateTitle(newButtonTitle)`, `newButtonTitle` is both being used as a temporary name for the message payload and the name of the field in the app `state`.  If they're named the same thing, we can use the shorthand `{...state, newButtonTitle}` instead of `{...state, newButtonTitle: newButtonTitle}`.

All that's left to define is the UI!  The list of buttons will render each button name in our `state` as a button which when clicked will signal the removal of that button:

```ocaml
{ReasonReact.array(
  self.state.buttons
  |> List.map(button =>
       <button
         key=button
         onClick={_ => {
           self.send(RemoveButton(button));
           Client.emit(socket, Msg(RemoveButton(button)));
         }}>
         {ReasonReact.string(button)}
       </button>
     )
  |> Array.of_list,
)}
```

We both send the `action` to our component's reducer as well as emit the `clientToServer` message to the server to make sure it gets removed everywhere.

Next up is the box to set the name of any new button created:

```ocaml
<input
  type_="text"
  value={self.state.newButtonTitle}
  onChange={evt =>
    self.send(UpdateTitle(ReactEvent.Form.target(evt)##value))
  }
/>
<button
  onClick={_ => {
    let name = self.state.newButtonTitle;
    self.send(UpdateTitle(""));
    self.send(AddButton(name));
    Client.emit(socket, Msg(AddButton(name)));
  }}>
  {ReasonReact.string("Add button " ++ self.state.newButtonTitle)}
</button>
```

Upon submitting, the component will reset the field to an empty string.

The last bit is the count of total connected clients:

```ocaml
{ReasonReact.string(
     (self.state.numClients |> string_of_int) ++ " connected",
 )}
```

And that's a wrap!  Let's fire it up.  Assuming you've had `yarn start:re` and `yarn start:bundle` running, open a new terminal and finally invoke `yarn serve`.  Now open up a couple of browser windows, point them all to `localhost:3000` and you should see them remain in sync with each other as you add and remove buttons.  Hooray!

Completed code can be found [here](https://github.com/deciduously/reason-buttons).

Cover image was found [here](https://blog.logrocket.com/what-makes-reasonml-so-great-c2c2fc215ccb).