How to use a promise-based API from a JavaScript library in ReasonML

Dmytro Gladkyi
4 min readMar 27, 2018

--

Let’s reuse a JS library, which exposes a promise-based API, in our ReasonML (ReasonReact) code. We will write bindings for a JS library and for its object methods.

As an example, we can take the Zoomdata JS SDK. It can connect to the backend database, construct data query, and then execute it and get some data from backend. All of these actions are completely asynchronous, so they all return JavaScript Promises.

This is how it looks in JavaScript:

To avoid extra configuration of webpack, I will use the library from global context (webpack 4 does not load UMD modules by default).

When Zoomdata SDK is loaded, it automatically installs itself into window.ZoomdataSDK. Or, if loaded by some bundlers, exposes itself as an normal module.

Create binding to global module

As a first step, we create binding to the global ‘Zoomdata’ value and to it’s ‘createClient’ method.

Binding to global values is pretty easy, you can check the official BuckleScript documentation: https://bucklescript.github.io/docs/en/bind-to-global-values.html

type tZoomdataSDK;/* global binding to UMD ZoomdataSDK */[@bs.val] external zoomdataSDK : tZoomdataSDK = "ZoomdataSDK";

Create bindings to module’s functions

We declared the dummy type ‘tZoomdataSDK’ for convenience. Now we will be able to ‘send’ messages to the zoomdataSDK:

[@bs.send]external createClient : (tZoomdataSDK, Js.Json.t) => tClient = "createClient";

We can read it as: “call method ‘createClient’ on object of type tZoomdataSDK, method takes some Js.Json value and returns the value of type tClient.

When we call it in ReasonML:

createClient(zoomdataSDK, someConfig)

It will be translated into this JavaScript:

ZoomdataSDK.createClient(someConfig)

‘createClient’ was declared using [@bs.send], the first argument to it is an object on which the ‘createClient’ method should be called with the given ‘someConfig’ value. The first argument also has our dummy type tZoomdataSDK, so that when we call createClient(zoomdataSDK), we pass a variable ‘zoomdataSDK’ of the same type — tZoomdataSDK.

You might have also noticed in the function definition is another type tClient

[@bs.send]external createClient : (tZoomdataSDK, Js.Json.t) => tClient = "createClient";

We will define it next.

Create bindings to a JavaScript object which represents the API namespace

Our ‘createClient’ function will return type tClient, which is defined like this:

tClient type has some JS class type with two methods available: createLiteQuery and runQueryForSourceId. This is 1 to 1 representation of th eSDK client object. It will be our bridge between the real JS object in runtime and our ReasonML type system! The SDK Client has other methods, but for our application we will use only these two.

Finally, let’s connect the ReasonML world to JavaScript via all these bindings by writing a simple function:

So, from ReasonML code, we call ‘makeClient’, provide application config (out of scope of this post), and it will return to us a promise, which resolves with instance of tClient, our bridge to JavaScript object!

Notice, how we used the pipe operator:

Encode.applicationConfig(input) |> createClient(zoomdataSDK, _)

The argument _ is optional, as a result the return value of Encode.applicationConfig(input) will go to the second argument of the createClient function, but I left it to show a new feature in ReasonML 3.1.0: “New pipe sugar allows inserting argument at arbitrary position”.

Using new bindings in ReasonML

In my ReasonReact component, I would like to use these bindings to get some data from the backend and print it to the console.

We have to make the following sequence: initialize tClient > createLiteQuery > runQuery.

First version of this sequence can be written in the following way (we will optimize it to 13 lines)

We defined a function in ReasonML. It takes two arguments: the id of the backend entity and the OAuth token to access the backend. As a result, the function returns a promise, which resolves to an array of ReasonML objects.

Now let’s make it more clear how it translates to JS, step by step.

Sdk.makeClient({
credentials: {
access_token: token,
},
application: Config.AppliedConfig.application,
)

This translates into JS:

ZoomdataSDK.createClient(
{credentials: {access_token: token}},
application: AppliedConfig
)

Remember, that Sdk.makeClient returns a promise, which resolves to an object of type tClient? Let’s use it to create a query! Next pipe looks like this:

This pipe expects the previous method to return Js.Promise, we wait on it and send it into the Js.Promise.then_ method. When the promise resolves, our then_ handler is called with an argument of type tClient.

Now we can use our bridge defined like this (just to remind you):

Pay attention, we resolve th epromise with a tuple:

|> Js.Promise.then_(query => Js.Promise.resolve((client, query)))

which has our client and query objects, as they are both needed for our last part!

The second step translates into such JS:

Last part, get data from the backend

The last step in the sequence just asks the client to run our query and to log a query to console (for debugging purpose).

Finally, we return an array of objects:

|> Js.Promise.then_(superResult => Js.Promise.resolve(superResult));

Usage from ReasonReact

And this is how we finally use the result of the promise sequence in a real ReasonReact application:

Enhance!

We could stop at this, but I never liked to return more than 1 entity from a Promise, so let’s rewrite our someResults function, to accept ‘client’ and ‘sourceId’. This will simplify our main sequence of promises:

We can simplify it even further:

// Before
|> Js.Promise.then_(client => someResults(client, sourceId))
// After: let's use Reason 3.1.0's partial application 'holes' feature:|> Js.Promise.then_(someResults(_, sourceId))

Final simplified promise sequence:

And the final reducer version:

You can see, how useful and easy-to-read partial application ‘holes’ are. We do not have to specify arrow functions in Js.Promise.then_ bodies, just specify which function to call and into which argument resolve value.

Final result

Bibliography

--

--