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:
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:
- Interoperating with JavaScript
- Using Promises
- 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
andsecret
come from a separate module namedEnvironment
- 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.