ReactJS

Reason in Practice

There's a lot to love about Reason, and you can read more about why we adopted it in a previous blog post. We've already reaped its benefits while incrementally converting a few React components using Reason React, but we wanted to dive deeper and explore what it's like to build with Reason first-class. Our app of choice for this exercise was an Unsplash mobile client using React Native. You can find the source code and installation instructions here.

Here's a screen recording of the app itself:
Demo

Areas for Discovery

At SmartLogic we build many unique applications for our clients, but they often share a few common aspects. Our goal was to learn how these can be accomplished while building an app purely in Reason. Specifically, we desired to gain a stronger practical understanding in three key areas:

  1. Interoperating with JavaScript
  2. Using Promises
  3. Consuming JSON

Also, as React is our JavaScript library of choice, the Reason React bindings naturally play a big part in this app (we won't get into too many of those details in this post, though).

Interop with JavaScript

One of Reason's strengths is seamless interoperability between itself and JavaScript. This is good news! Reason is relatively new so there are many bindings begging to be written, but you can still hit the ground running and easily use existing JS libraries within your Reason. This is mostly done using BuckleScript's external.

Unsplash provides a JS client, so to use it within our app we have to create a Reason binding:

/* Unsplash.re */

type unsplashConfig = {.
  "applicationId": string,
  "secret": string,
  "callbackUrl": string
};
type unsplash = {.
  "collections": {.
    [@bs.meth] "listCollections": (int, int, string) => Js.Promise.t(Fetch.Response.t)
  },
  "photos": {.
    [@bs] [@bs.meth] "listPhotos": (int, int, string) => Js.Promise.t(Fetch.Response.t)
  }
};
[@bs.new] [@bs.module "unsplash-js/native"] external createUnsplashClient : unsplashConfig => unsplash = "default";

let appId = Environment.appId;
let secret = Environment.secret;
let callbackUrl = "urn:ietf:wg:oauth:2.0:oob";

let unsplash = createUnsplashClient({
  "applicationId": appId,
  "secret": secret,
  "callbackUrl": callbackUrl
});

let listPhotos = unsplash##photos##listPhotos;

Note that this binding is not very big especially compared to the Unsplash JS library. We've only bound what we care to use from the library for this app, namely listing collections and photos. These two Unsplash methods (out of many) are the only ones which we've given type definitions, so it's all that is "exposed" to our app. Calling any other Unsplash JS methods will result in a Reason compiler error.

We use the [@bs.module] BuckleScript attribute to import a JS module into our Reason app. Depending on how the module is exported, there are a couple of ways to do this. The Unsplash JS module uses ES2015 default exports, so we need to import it accordingly.

There's one more thing for this particular import, though. Unsplash JS provides a constructor (of the JS sense, not to be confused with a Reason constructor) that instantiates an API client (i.e. const unsplash = new Unsplash(...)). Since the JS module we're importing is a class, as opposed to a function or plain value, we need to use @bs.new as well. BuckleScript attributes may be composed of one more more, so we end up with what's on line 16. We can now use createUnsplashConfig on line 22 as if it were a regular function we defined ourselves.

Here's a rundown of our entire Reason Unsplash module:

  • Line 3-15 - define types for the Unsplash config and the Unsplash class, both of type Object as 'Record'
  • Line 16 - import unsplash-js JS module
  • Line 18-20 - declare config variables where appId and secret come from a separate module named Environment
  • Line 22-26 - create the actual Unsplash API client
  • Line 28 - expose a method that lists photos to whomever uses this module, accessible via dot notation, i.e. Unsplash.listPhotos(...)

This may seem like a lot of work just to use two methods from a JS module, but it's all necessary for Reason to guarantee us 100% type safety.

Using Promises

So we have our Reason module that wraps the Unsplash API client. Great! How do we use it?

Promises!

BuckleScript provides bindings to JS promises with its Js.Promise library. You may have noticed it earlier in our Unsplash module, where both methods listCollections and listPhotos have the return type Js.Promise.t(Fetch.Response.t). The Fetch.Response type here comes from bs-fetch which provides bindings to the Fetch API. Together, these two libraries form our basis for making asynchronous calls in Reason.

In our root React component, App.re, we have the following function:

let fetchData = (~page: int, ~callback) =>
  Js.Promise.(
    ([@bs] Unsplash.listPhotos(page, 16, "trending"))
    |> then_(Fetch.Response.json)
    |> then_(json => resolve(callback(json)))
    |> catch((err) => resolve(Js.log(err)))
  );

There's a lot to unpack here, but it's fairly readable. We have a pipe chain that calls the Unsplash API via our Unsplash module, transforms the fetch response to JSON a la res.json(), and eventually resolves on success or catches on error.

The [@bs] attribute is needed because of drawbacks related to how JS can mess with currying in Reason. You can read more about that here.

The resolve function comes from Js.Promise. We are able to access it by using a local open. Otherwise, our fetchData function would have to be written more explicitly as

let fetchData = (~page: int, ~callback) =>
  ([@bs] Unsplash.listPhotos(page, 16, "trending"))
  |> Js.Promise.then_(Fetch.Response.json)
  |> Js.Promise.then_(json => Js.Promise.resolve(callback(json)))
  |> Js.Promise.catch((err) => Js.Promise.resolve(Js.log(err)));

which is much more verbose!

Figuring out Reason promises was mostly trial and error in our experience. While BuckleScript docs and official Reason docs are fantastic overall, they fall a little short here. bs-fetch's examples were helpful, though, and we recommend checking those out.

Consuming JSON

Now that we're consuming the Unsplash API, let's get to working with its data. Our app has a module dedicated to parsing JSON.

module Decode = {
  let urls = json =>
    Json.Decode.{
      full: json |> field("full", string),
      regular: json |> field("regular", string),
      small: json |> field("small", string),
      thumb: json |> field("thumb", string)
    };

  let photo = json =>
    Json.Decode.{
      id: json |> field("id", string),
      description: json |> optional(field("description", string)),
      likes: json |> field("likes", int),
      urls: json |> urls
    };
};

Like with promises, BuckleScript provides JSON bindings in their Js.Json library. However, using this library alone makes for even more convoluted code as seen in examples.

Instead, we'll use the library bs-json to help us decode our JSON. bs-json allows you to decode JSON structures at simple levels then compose them to form more complex decoders. We employ this strategy when decoding our listPhotos API response from Unsplash for which the response looks like this:

[
  {
    "id": "qiZ_3myAsRE",
    "created_at": "2018-01-30T10:08:42-05:00",
    "updated_at": "2018-01-30T10:10:43-05:00",
    "width": 6000,
    "height": 4000,
    "color": "#E4C6AD",
    "description": null,
    "categories": [],
    "urls": {
      "raw": ...,
      "full": ...,
      "regular": ...,
      "small": ...,
      "thumb": ...
    },
    "links": {...},
    "liked_by_user": false,
    "sponsored": false,
    "likes": 4,
    "user": {...}
    "current_user_collections": [...]
  },
  ...
]

We only decode a subset of keys: id, description, likes, and urls. The first three map to primitive types, and it's straightforward to decode them using bs-json's Json.Decode module. We have to do a little more for urls which is a nested object. The nice thing about bs-json is that it allows us to compose our decoder functions (urls in this case)--a friendly approach for nested JSON.

Conclusion

There is more to this Reason app than what we've covered, but we've addressed three real-world challenges that we encounter in our app development. Check out the source and poke around for more! For us at SmartLogic, building this app in 100% Reason reinforced our confidence in the language and confirmed its technical feasibility when building custom applications for our clients.

You've successfully subscribed to SmartLogic Blog
Great! Next, complete checkout for full access to SmartLogic Blog
Welcome back! You've successfully signed in.
Unable to sign you in. Please try again.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.