Adding ReasonML to a React TypeScript codebase

yakimych

Kyrylo Yakymenko

Posted on May 26, 2019

Adding ReasonML to a React TypeScript codebase

The most natural way to introduce ReasonML into an existing React codebase is by writing a few components in ReasonReact. Nowadays, with 0.7.0 and support for react hooks, it's not that different from writing components in JavaScript. In this article, however, we are going to explore an alternative way into an existing codebase — creating an API-calling-and-decoding layer in Reason.

Note: This is the third 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. Curious about using Reason in a Vue.js codebase? The second article, Adding ReasonML to a Vue application, explains just that 😃

Step 0: Starting point

Our starting point is a React application created via create-react-app. This guide will work equally well for a pure JavaScript codebase, but to make things a bit tricker, let's say this is a TypeScript application — this will require our API-calling functions to generate TypeScript types, rather than plain JavaScript. Good news — genType integration has become much easier with BuckleScript 5.

Step 1: Adding BuckleScript

We are going to need BuckleScript for compiling ReasonML or OCaml code to JavaScript and genType in order to generate TypeScript types. 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": {
  "start": "bsb -make-world && react-scripts start",
  "build": "bsb -make-world && react-scripts 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-react-typescript",
  "sources": [
    {
      "dir": "src/reason",
      "subdirs": true
    }
  ],
  "package-specs": [
    {
      "module": "es6-global",
      "in-source": true
    }
  ],
  "suffix": ".bs.js",
  "namespace": true,
  "refmt": 3,
  "gentypeconfig": {
    "language": "typescript"
  }
}
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.tsx file next to TestFunctions.bs.js:

[@genType]
let reasonSum = (a, b) => a + b;
Enter fullscreen mode Exit fullscreen mode
// tslint:disable-next-line:no-var-requires
const Curry = require("bs-platform/lib/es6/curry.js");

// tslint:disable-next-line:no-var-requires
const TestFunctionsBS = require("./TestFunctions.bs");

export const reasonSum: (_1: number, _2: number) => number = function(
  Arg1: any,
  Arg2: any
) {
  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 or TypeScript — let's call it from our React component:

import * as React from "react";
import { reasonSum } from "./reason/TestFunctions.gen";

export const TestComponent = () => (
  <div>Result of a ReasonML function call: {reasonSum(1, 2)}</div>
);
Enter fullscreen mode Exit fullscreen mode

It is possible to import reasonSum from TestFunctions.bs.js instead, if we were working with a pure JavaScript codebase. In this case, we won't get any type information.

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, your 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-fetch and decode the response with bs-json:

npm install --save bs-fetch @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-fetch"]
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

This generates a numberFact type in TypeScript:

export type numberFact = {
  readonly number: number;
  readonly text: string;
  readonly isFound: boolean;
};
Enter fullscreen mode Exit fullscreen mode

The API call itself can be performed this way:

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

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

export const fetchNumberFact: (_1: void) => Promise<numberFact> = function(
  Arg1: any
) {
  const result = NumberFactsBS.fetchNumberFact(Arg1);
  return result.then(function _element($promise: any) {
    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 React component and display the result:

import React, { useState, useEffect } from "react";
import {
  numberFact as NumberFact,
  fetchNumberFact
} from "./reason/NumberFacts.gen";

export const App: React.FC = () => {
  const [numberFact, setNumberFact] = useState<NumberFact | null>(null);

  const fetchNewFact = () =>
    fetchNumberFact()
      .then(newFact => setNumberFact(newFact))
      .catch(e => console.log("Error fetching number fact: ", e));

  useEffect(() => {
    fetchNewFact();
  }, []);

  return (
    <div className="App">
      {numberFact === null ? (
        "Loading initial number fact..."
      ) : (
        <div className="number-fact">
          <div>Number: {numberFact.number}</div>
          <div>Fact: "{numberFact.text}"</div>
          <div>{numberFact.isFound ? "Found" : "Not found!"}</div>
          <button onClick={fetchNewFact}>Fetch new fact</button>
        </div>
      )}
    </div>
  );
};
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 React 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 React components. This approach is an alternative to jumping straight into ReasonReact (in case that seems too big of a step). The source code is available on GitHub.

The same approach can be used for adding ReasonML to a Vue.js application, or pretty much any other JavaScript application.

💖 💪 🙅 🚩
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