Adding ReasonML to a Vue application

yakimych

Kyrylo Yakymenko

Posted on May 26, 2019

Adding ReasonML to a Vue application

Despite the fact that ReasonML is a natural fit for React, thanks to BuckleScript it can easily be used in any JavaScript application. And yes — this includes Vue.js!

If you're working with Vue and like OCaml/ReasonML, or whether you've heard all the hype and are curious to try — in this article I will show how to use code written in Reason from Vue.

Note: This is the second article in my miniseries about integrating Reason into an existing codebase. For a more basic explanation about how everything hangs together, check out the first article: Adding ReasonML to an existing codebase. In Part 3 we're going to integrate Reason into a React TypeScript codebase.

Step 0: Starting point

Our starting point is a freshly created Vue application with the help of the Vue CLI default preset. It is worth noting that thanks to genType, this guide would work equally well for a TypeScript application.

Step 1: Adding BuckleScript

We are going to need BuckleScript for compiling ReasonML or OCaml code to JavaScript and genType in order to simplify interop between Reason and JS. More about this in Part 1 of the mini series.

Let's go ahead and install the packages:

npm install --save-dev bs-platform gentype
npm install -g bs-platform
Enter fullscreen mode Exit fullscreen mode

We're going to need to make sure bucklescript runs before babel, so let's add the command to the start and build scripts in package.json:

"scripts": {
  "serve": "bsb -make-world && vue-cli-service serve",
  "build": "bsb -make-world && vue-cli-service build"
}
Enter fullscreen mode Exit fullscreen mode

The last thing left before we can start writing code is to add bsconfig.json:

{
  "name": "reason-in-vue",
  "sources": [
    {
      "dir": "src/reason",
      "subdirs": true
    }
  ],
  "package-specs": [
    {
      "module": "es6-global",
      "in-source": true
    }
  ],
  "suffix": ".bs.js",
  "namespace": true,
  "refmt": 3,
  "gentypeconfig": {
    "language": "untyped"
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Writing a function in Reason

Note that src/reason is specified as the sources directory, so let's create it and add a TestFunctions.re file so that we can test our setup:

let reasonSum = (a, b) => a + b;
Enter fullscreen mode Exit fullscreen mode

If you're using VS Code with the reason-language-server extension, a TestFunctions.bs.js file will immediately get generated next to the .re file:

function reasonSum(a, b) {
  return (a + b) | 0;
}
Enter fullscreen mode Exit fullscreen mode

Annotating the function with [@genType] would produce a TestFunctions.gen.js file next to TestFunctions.bs.js:

[@genType]
let reasonSum = (a, b) => a + b;
Enter fullscreen mode Exit fullscreen mode
import * as Curry from "bs-platform/lib/es6/curry.js";

import * as TestFunctionsBS from "./TestFunctions.bs";

export const reasonSum = function(Arg1, Arg2) {
  const result = Curry._2(TestFunctionsBS.reasonSum, Arg1, Arg2);
  return result;
};
Enter fullscreen mode Exit fullscreen mode

At this point we can use the reasonSum function from JavaScript — let's call it from our Vue component:

<template>
  <div id="app">
    <div>The result is {{ result }}</div>
  </div>
</template>

<script>
import { reasonSum } from "./reason/TestFunctions.gen";

export default {
  name: "app",
  data() {
    return {
      result: reasonSum(1, 2)
    };
  }
};
</script>
Enter fullscreen mode Exit fullscreen mode

Note that if you're running from the terminal and would like changes in Reason files to get transpiled and picked up on the fly, we would need to have bsb -make-world -w running in the background:

Compilation on the fly

Step 3: Calling the API and decoding the response in Reason

The next step is adding an API call that will fetch some interesting information about a random number from http://numbersapi.com.

A call to http://numbersapi.com/random/math?json would produce the following response:

{
  "text": "880 is the number of 4×4 magic squares.",
  "number": 880,
  "found": true,
  "type": "math"
}
Enter fullscreen mode Exit fullscreen mode

We're going to make the API call with bs-axios and decode the response with bs-json:

npm install --save bs-axios @glennsl/bs-json
Enter fullscreen mode Exit fullscreen mode

An important step that is easy to forget is adding those dependencies to bsconfig.json:

  "bs-dependencies": ["@glennsl/bs-json", "bs-axios"]
Enter fullscreen mode Exit fullscreen mode

Now we can create a new file NumberFacts.re, model the type, and create a decoder:

[@genType]
type numberFact = {
  number: int,
  text: string,
  isFound: bool,
};

module Decode = {
  let fact = json =>
    Json.Decode.{
      number: json |> field("number", int),
      text: json |> field("text", string),
      isFound: json |> field("found", bool),
    };
};
Enter fullscreen mode Exit fullscreen mode

The API call itself can be performed this way:

[@genType]
let fetchNumberFact = () =>
  Js.Promise.(
    Axios.get("http://numbersapi.com/random/math?json")
    |> then_(response => response##data |> Decode.fact |> resolve)
  );
Enter fullscreen mode Exit fullscreen mode

The inferred type in Reason is unit => Js.Promise.t(numberFact), as expected. The generated JavaScript code in NumberFacts.gen.js function looks like this:

import * as NumberFactsBS from "./NumberFacts.bs";

export const fetchNumberFact = function(Arg1) {
  const result = NumberFactsBS.fetchNumberFact(Arg1);
  return result.then(function _element($promise) {
    return { number: $promise[0], text: $promise[1], isFound: $promise[2] };
  });
};
Enter fullscreen mode Exit fullscreen mode

I explain the differences between the code generated by BuckleScript and genType in the first article of this miniseries.

Step 4: Tying it all together

This is all we have to do on the Reason side of things. Now it is time to call our function from the Vue component and display the result:

<template>
  <div id="app">
    <div class="number-fact">
      <div>Number: {{numberFact.number}}</div>
      <div>Fact: "{{numberFact.text}}"</div>
      <div>{{numberFact.isFound ? "Found" : "Not found!"}}</div>
      <button @click="fetchNewFact">Fetch new fact</button>
    </div>
  </div>
</template>

<script>
import { fetchNumberFact } from "./reason/NumberFacts.gen";

export default {
  name: "app",
  data() {
    return {
      numberFact: {
        number: -1,
        text: "-1 is not an interesting number",
        isFound: false
      }
    };
  },
  methods: {
    fetchNewFact: function() {
      fetchNumberFact().then(numberFact => (this.numberFact = numberFact));
    }
  },
  mounted: function() {
    this.fetchNewFact();
  }
};
</script>
Enter fullscreen mode Exit fullscreen mode

A new fact will be automatically loaded after the component is mounted. Clicking the "Fetch new fact" button would load a fresh random number fact — all done via ReasonML code.

Summary

Adding ReasonML to an existing Vue codebase can be done in a matter of minutes. After this initial setup, it becomes possible to write logic in ReasonML or OCaml and use it in existing Vue components. The source code is available on GitHub.

Hopefully this tutorial will inspire Vue.js developers to try ReasonML!

💖 💪 🙅 🚩
yakimych
Kyrylo Yakymenko

Posted on May 26, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related