ReasonML with GraphQL, the Future of Type-Safe Web Applications

iwilsonq

Ian Wilson

Posted on March 13, 2019

ReasonML with GraphQL, the Future of Type-Safe Web Applications

graphql plus ReasonML
ReasonML, also known as JavaScript-flavored OCaml, offers nearly impenetrable type safety for developing user interfaces. By adopting a static type system, you can eliminate an entire class of errors before your app is served.

We’re going to look into building a small web application that uses consumes a GraphQL endpoint using ReasonML. We’ll cover:

  • getting started with a ReasonReact project
  • setting up a client with reason-apollo
  • sending queries
  • mutating data

If you’re new to both GraphQL and ReasonML, I’d suggest learning one at a time. Learning more than one big thing at once is often difficult for me. If you’re experienced with JavaScript and GraphQL, but want to learn ReasonML, read on, but keep the docs handy.

Getting Started - Establishing a ReasonReact Project

In order to get going with ReasonML, we must first install the cli, bsb-platform that handles bootstrapping the project. You should also get an editor plugin that helps with developing ReasonML apps. If you’re using VSCode, reason-vscode by Jared Forsyth is my preferred plugin.

npm install -g bsb-platform
Enter fullscreen mode Exit fullscreen mode

This installs the BuckleScript compiler that turns our ReasonML into wonderful JavaScript which has already been type-checked and can be run in the browser.

Now we can initialize our project and hop right in.

bsb -init reason-graphql-example -theme react
cd reason-graphql-example
npm install
Enter fullscreen mode Exit fullscreen mode
  • The init argument specifies the name of the project we’re initializing.
  • The theme argument specifies the template we wish to use. I usually just choose the react theme.
  • We run npm install to install dependences just like in any other JavaScript project.

With the project scaffolded, we can try to build it. In two separate terminal panes, run:

npm start
# and
npm run webpack
Enter fullscreen mode Exit fullscreen mode
  • npm start tells BuckleScript (bsb) to build the project watch for changes to your .re files.
  • npm run webpack fires up webpack to build your main JavaScript bundle

Quick tip: you’ll notice that the webpack output is in the build folder but the index.html file is in the src folder. We can make serving the project a little bit easier by moving the index.html file to the build folder and rewriting the script tag to point at the adjacent Index.js file. 

With all that taken care of, you can serve your build folder using http-server build or serve build and check out the project.

initial screen

When I’m developing a ReasonML project, I’m running 3 terminal tabs:

  1. npm start to transpile ReasonML to JavaScript
  2. npm run webpack to bundle JavaScript
  3. serve build to actually serve the build on a port

Before we can get to the fun stuff, we still must clear out the boilerplate and set up react-apollo.

Go ahead and remove the Component1 and Component2 files, and then update Index.re to the following:

ReactDOMRe.renderToElementWithId(<App />, "root");
Enter fullscreen mode Exit fullscreen mode

Update index.html to:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>ReasonML GraphQL Example</title>
  </head>
  <body>
    <div id="root"></div>

    <script src="./Index.js"></script>
  </body>
</html>

Enter fullscreen mode Exit fullscreen mode

Finally, create an App.re file and add the following:

/* App.re */
let str = ReasonReact.string;
let component = ReasonReact.statelessComponent("App");

let make = _children => {
  ...component,
  render: _self => 
    <div> 
      <h1> {"Hello ReasonReact" |> str} </h1>
    </div>
};
Enter fullscreen mode Exit fullscreen mode

You might have to re-run your terminal commands, but with all that said and done, you should have something like this appear on your screen:

hello reason react

It feels like a lot of effort to get started, but accepting early friction for a smoother experience later on is the tradeoff here.

Initializing Reason Apollo

In order to get set up with Apollo we’re going to run:

npm install -S reason-apollo react-apollo apollo-client apollo-cache-inmemory apollo-link apollo-link-context apollo-link-error apollo-link-http graphql graphql-tag apollo-link-ws apollo-upload-client subscriptions-transport-ws
Enter fullscreen mode Exit fullscreen mode

That looks like a big install command. It is, but only the first package, reason-apollo, is consumed in our ReasonML code. However, reason-apollo is a library of binding labels depends on these other JavaScript packages.

In order to make writing GraphQL queries more friendly we’ll need one more dev dependency.

npm install -D graphql_ppx
Enter fullscreen mode Exit fullscreen mode

As that installs we can open up our bsconfig.json file and update the “bs-dependencies” and “ppx-flags” keys like so:

// bsconfig.json
{
    "bs-dependencies": [
        "reason-react",
        "reason-apollo"
    ],
    "ppx-flags": [
        "graphql_ppx/ppx"
    ],

    // other fields...
}
Enter fullscreen mode Exit fullscreen mode

The “bs-dependencies” array tells BuckleScript to include those npm modules in the build process. The ppx flags array lets our IDE know how to handle preprocess certain directives, GraphQL in our case.

Create a file inside the src folder called Client.re. This is where we will declare our instance of the Apollo Client.

/* Client.re */
let inMemoryCache = ApolloInMemoryCache.createInMemoryCache();

let httpLink =
  ApolloLinks.createHttpLink(~uri="https://video-game-api-pvibqsoxza.now.sh/graphql", ());

let instance =
  ReasonApollo.createApolloClient(~link=httpLink, ~cache=inMemoryCache, ());
Enter fullscreen mode Exit fullscreen mode

Note: If this uri, https://video-game-api-pvibqsoxza.now.sh/graphql does not work, please send me a message on twitter or here in the comments and I’ll update that as quickly as possible

When we work with ReasonML, any variable that we create with a let binding is automatically exported from the module for us.

With the instance created, we can reference it in any of our other .re files. Update Index.re to the following:

/* Index.re */
ReactDOMRe.renderToElementWithId(
  <ReasonApollo.Provider client=Client.instance>
    <App />
  </ReasonApollo.Provider>,
  "root",
);
Enter fullscreen mode Exit fullscreen mode

It looks a little like a standard React JS application, with a couple caveats. Notice that there are no import statements. In ReasonML, we have access to all of the namespaces built in our application. From the perspective of Index.re, We can see the Client and the App modules.

When we create a .re file in our src folder, it becomes a module. We could also declare our modules explicitly within our files.

It is now time to consume our API.

Sending Queries and Rendering a List

While writing this article I created a small Node GraphQL server, the code of which is available at this repo. To keep costs low, I declared an array of mock data to return on each GraphQL request rather than host a database.

Rather than create a todo app, I decided to create a list of video games that I played at some point long ago. Then, I could check if I finished it or not, thus remembering the games that I still haven’t beat.

As we are working with a GraphQL server, we should be able to figure out exactly how to call it by observing the schema.

  type VideoGame {
    id: ID!
    title: String!
    developer: String!
    completed: Boolean!
  }

  type Query {
    videoGames: [VideoGame!]!
  }

  type Mutation {
    completeGame(id: ID!): VideoGame!
  }
Enter fullscreen mode Exit fullscreen mode

Currently, we have one query and one mutation, both of which operate around this VideoGame type. A GraphQL adept will notice that every return value is non-nullable, that is, these responses cannot return unset fields or null objects.

Soon we’ll see why all of the !’s are particularly important for our ReasonML code.

Let’s begin by defining the query in on top of App.re, just below the component declaration.

/* App.re */ 

module VideoGames = [%graphql
  {|
  query VideoGames {
    videoGames {
      id
      title
      developer
      completed
    }
  }
|}
];

module VideoGamesQuery = ReasonApollo.CreateQuery(VideoGames);

/* let make = ... */
Enter fullscreen mode Exit fullscreen mode

Comparing with the JavaScript in react-apollo, this code would be most analogous to:

const VideoGames = gql`
  query VideoGames {
    videoGames {
      id
      title
      developer
      completed
    }
  }
`

// later in render
render() {
  return (
    <Query query={VideoGames}> {/* ... */} </Query>
  )
}

Enter fullscreen mode Exit fullscreen mode

Now let’s update the render function:

/* App.re */
let make = _children => {
  ...component,
  render: _self => {
    let videoGamesQuery = VideoGames.make();
    <div> 
        <h1> {"ReasonML + ReasonReact + GraphQL" |> str} </h1>
        <VideoGamesQuery variables=videoGamesQuery##variables>
        ...{
             ({result}) =>
               switch (result) {
               | Loading => <div> {"Loading video games!" |> str} </div>
               | Error(error) => <div> {error##message |> str} </div>
               | Data(data) => <VideoGameList items=data##videoGames />
               }
           }
      </VideoGamesQuery>
    </div>;
  }
};
Enter fullscreen mode Exit fullscreen mode

Here, we’re taking advantage of ReasonML’s coolest feature - pattern matching. Pattern matching combined with variants makes the logic that you would otherwise put in branches of if-else statements more linear and easier to follow. It is also reduces branch checking to constant rather than linear time, making it more efficient.

If the ReasonML code ever seems more verbose, just remember we’re still getting perfect type safety when it compiles. We still need to build the VideoGamesList component as well as define the videoGame record type.

Starting with the record type, create a new file called VideoGame.re and add the following:

/* VideoGame.re */

[@bs.deriving jsConverter]
type videoGame = {
  id: string,
  title: string,
  developer: string,
  completed: bool,
};
Enter fullscreen mode Exit fullscreen mode

The videoGame type as we have it here, has 4 fields, none of which are optional. The BuckleScript directive above it adds a pair of exported utility methods that allow us to convert between ReasonML records and JavaScript objects.

Tip: when Apollo returns a response, it returns untyped JavaScript objects. The jsConverter directive gives us an exported method called videoGameFromJs that we can use to map the Apollo query data to fully typed ReasonML.

To see this mechanic in action, create a new file called VideoGameList.re and add:

/* VideoGameList.re */
open VideoGame;

let str = ReasonReact.string;
let component = ReasonReact.statelessComponent("VideoGameList");

let make = (~items, _children) => {
  ...component,
  render: _self =>
    <ul style={ReactDOMRe.Style.make(~listStyleType="none", ())}>
      {
        items
        |> Array.map(videoGameFromJs)
        |> Array.map(item =>
             <li key={item.id}>
                <input
                  id={item.id}
                  type_="checkbox"
                  checked={item.completed}
                />
                <label htmlFor={item.id}>
                  {item.title ++ " | " ++ item.developer |> str}
                </label>
             </li>
           )
        |> ReasonReact.array
      }
    </ul>,
};
Enter fullscreen mode Exit fullscreen mode
  1. Open the VideoGame module (VideoGame.re) at the top so we can use all of it’s exports in the VideoGameList module.
  2. Declare the component type and string rendering shorthand.
  3. Define a make function that expects one prop, items.
  4. Inside the render function, pipe the items to convert JS objects to ReasonML records, map records to JSX, and finally output them as an array.

Note: Piping basically reverses the order of function calls to potentially improve readability. With the |> operator, the items object is applied to each function as the last argument.

Though I like prefer the piping style, the following are equivalent.

items 
    |> Array.map(videoGameFromJs)
    |> Array.map(renderItem)
    |> ReasonReact.array;

ReasonReact.array(
    Array.map(
        renderItem,
        Array.map(
            videoGameFromJs,
            items
        )
    )
);
Enter fullscreen mode Exit fullscreen mode

I think we are ready to once again compile and serve our project. If you haven’t already, run this command in your project root:

yarn send-introspection-query https://video-game-api-pvibqsoxza.now.sh/graphql
Enter fullscreen mode Exit fullscreen mode

This generates a graphql_schema.json file that Reason Apollo uses to type check your queries. If your ReasonML app asks for a field that isn’t on the schema, or if it doesn’t properly handle optional data types, it will not compile.

The strict typing serves as a wonderful sanity check for writing queries and mutations.

When all is said and done, you should see the following.

finished screen

Don’t yell at me for not finishing the main story in Skyrim.

Mutating Data

One thing you may notice thus far is that clicking the checkboxes doesn’t do anything. This is expected, since we have not yet wired up a mutation.

Let us begin by recalling our schema above, and creating a module for the mutation to mark a game completed.

Inside VideoGameList.re, add these modules to the top of the file just beneath the call to create a component.

/* VideoGameList.re */
module CompleteGame = [%graphql
  {|
  mutation CompleteGame($id: ID!) {
    completeGame(id: $id) {
      id
      completed
    }
  }
|}
];

module CompleteGameMutation = ReasonApollo.CreateMutation(CompleteGame);
Enter fullscreen mode Exit fullscreen mode

For the render prop of the mutation, it’ll look pretty similar to the JavaScript version. I’ll put this code here and then walk through it, starting from inside the <li> tag.

/* VideoGameList.re */

<li key={item.id}>
  <CompleteGameMutation>
    ...{
        (mutate, {result}) => {
          let loading = result == Loading;
          <div>
            <input
              id={item.id}
              type_="checkbox"
              checked={item.completed}
              onChange={
                _event => {
                  let completeGame =
                    CompleteGame.make(~id=item.id, ());
                  mutate(~variables=completeGame##variables, ())
                  |> ignore;
                }
              }
            />
            <label
              htmlFor={item.id}
              style={
                ReactDOMRe.Style.make(
                  ~color=loading ? "orange" : "default",
                  (),
                )
              }>
              {item.title ++ " | " ++ item.developer |> str}
            </label>
          </div>;
        }
      }
  </CompleteGameMutation>
</li>
Enter fullscreen mode Exit fullscreen mode

Like the Apollo VideoGamesQuery component we used earlier, the CompleteGameMutation component we see here passes to its children a mutate function as well as a results object.

This particular component is not the best example to show off how you could use that results object, as I only take advantage of it when a single item is being updated. If it is, I color the text of the item label green and call that the loading state.

I’m no UX guy, but I think that’ll do for today.

Wrapping Up

ReasonML is a pretty powerful and expressive language. If you are new to ReasonML and itching to build some type safe user interfaces, here are some resources to learn from:

  1. Many of the third party tools we use in JavaScript come out of the box with ReasonML. This article by David Kopal explains how, along with some other reasons why writing ReasonML is so cool.
  2. Jared Forsyth’s blog has great content about ReasonML and OCaml. He is one of the most active contributors to the community.
  3. I get most of my learning done through the ReasonML docs and the BuckleScript docs. They are easy to follow and contain powerful insights on the design choices when implementing language features.

If you want to quickly set up your own GraphQL server then check out my other article, Learn to Build a GraphQL Server with Minimal Effort.

I hope to write more articles about ReasonML and GraphQL in the future. If these interest you then by all means follow me on Medium and on Twitter!

This article was originally published in Open GraphQL on Medium.

💖 💪 🙅 🚩
iwilsonq
Ian Wilson

Posted on March 13, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related