Absinthe subscriptions with ReasonML and Urql

Lately I've been looking into ReasonML as a language to pick up. In the past I've dabbled with Elm and really enjoyed it but it's lack of interoperability with javascript hampers its development. ReasonML offers a far better story in this regards, there are excellent React and React-Native libraries/bindings available as well as bindings for popular Graphql clients. That, combinated with great type-safety makes it a compelling alternative to Elm and Typescript.

So, to followup on my last post I will now show how to make ReasonML bindings for the Urql graphql client in combination with Absinthe. Urql has Reason bindings available but there are none for the combination with Absinthe so I'll show how to add those. It follows the same code as the previous post, but now in ReasonML

I'll step through the code basically line by line.

First we need to create the connection to the Phoenix backend.

const phoenixSocket = new PhoenixSocket("ws://localhost:4000/socket");

This translates to the following:

// index.re
let socket = PhoenixSocket.make("ws://localhost:4000/socket");

though we still need a binding to the PhoenixSocket library.

// PhoenixSocket.re
type t;

[@bs.module "phoenix"] [@bs.new] external make: string => t = "Socket";

[@bs.new] is for [when you need to bind to a javascript constructor. There are also bindings for Phoenix available on NPM

Next line is

const absintheSocket = withAbsintheSocket.create(phoenixSocket);

this converts to:

// index.re
let absintheSocket = AbsintheSocket.create(socket);

Again, we need to create bindings for the library we use, in this case AbsintheSocket:

// AbsintheSocket.re
type t;

type absintheSocket;

type payload = {
  operation: string,
  variables: option(Js.Json.t),
};

type notifier;

type observerLike('value) = {
  next: 'value => unit,
  error: Js.Exn.t => unit,
  complete: unit => unit,
};

type unsubscribe = {unsubscribe: unit => unit};

type observableLike('value) = {
  subscribe: observerLike('value) => unsubscribe,
};

[@bs.module "@absinthe/socket"]
external create: PhoenixSocket.t => absintheSocket = "create";

[@bs.module "@absinthe/socket"]
external send: (absintheSocket, payload) => notifier = "send";

[@bs.module "@absinthe/socket"]
external toObservable: (absintheSocket, notifier) => ReasonUrql.Client.Exchanges.observableLike('value) =
  "toObservable";

That's it, you can see the external keyword is mapping the binding to the original @absinthe/socket library on npm.

What remains is adding the AbsintheSocket to the Urql client and subscription configuration. This is the same as in the previous article but converted to ReasonML, so check that for a comparison. It also shows how add the subscription exchange to Urql and tie it into React.

See below:

// index.re
let socket = PhoenixSocket.make("ws://localhost:4000/socket");

let absintheSocket = AbsintheSocket.create(socket);

let subscriptionExchangeOpts =
  Client.Exchanges.{
    forwardSubscription: operation => {
      let notifier =
        AbsintheSocket.send(
          absintheSocket,
          {operation: operation.query, variables: operation.variables},
        );
      let observer = AbsintheSocket.toObservable(absintheSocket, notifier);

      observer;
    },
  };

let subscriptionExchange =
  Client.Exchanges.subscriptionExchange(subscriptionExchangeOpts);

let urql_client =
  Client.make(
    ~url="http://localhost:4000/api",
    ~exchanges=
      Array.append(
        Client.Exchanges.defaultExchanges,
        [|subscriptionExchange|],
      ),
    (),
  );

ReactDOMRe.render(
  <Context.Provider value=urql_client> <ReasonGraphql /> </Context.Provider>,
  makeContainer("Reason graphql"),
);

It can feel a little awkward writing these bindings and there are not a lot of resources on how to do it or many packages already written for you. However, once you get the hang of it, it's pretty straightforward.

If you've ever worked with Elm and appreciate its, ‘if it compiles, it works’ motto then you'll also appreciate ReasonML. Its guarantees about your code working are not on the same level but it does provide you with great interop with the javascript ecosystem. Within the ReasonML code you write there is a high standard of safety and you can follow the compiler errors till your code works :)

Certainly try it out! It may be daunting at first and even if you are not using it in your next project it will have been a informative couple of hours.