DEV Community

John Jackson
John Jackson

Posted on • Updated on

Reason, GraphQL PPX, and Gatsby

In the past few months, a lot of work has gone into the GraphQL PPX, and it’s shaping up to become a tremendously useful project. I’ve been testing it out on a small Gatsby site. This post describes some of the issues I discovered along the way, along with how I addressed them.

⚠️ Beta warning

GraphQL PPX is in beta and is not stable, and this post will likely be out of date sooner than later. Readers from the future: consult the GraphQL PPX docs over this post!

Reason? PPX? What are these?

I’m assuming most readers are familiar with these technologies, but here’s a clarification for any newcomers.

Reason

Reason (often called ReasonML) is a language with JavaScript-like syntax that uses BuckleScript to compile to JavaScript. It’s often compared to TypeScript since it feels like writing type-checked JavaScript, but it’s really an extension of OCaml, which means it will be always be 100% type-safe.

PPX

A PPX is a macro in Reason (or OCaml). PPXs typically generate code that would be tedious or impractical to write by hand. For ReasonReact users, some familiar PPXs included with BuckleScript are [@react.component] and -> (pipe-first).

Gatsby and GraphQL

Gatsby is a static-site-generator that uses GraphQL to query data and React to build page markup.

Using Reason and Gatsby together, and the problem with that

For the most part, using Reason with Gatsby is just like using Reason with any other JavaScript package. As long as you write bindings to the Gatsby functions, then your Reason modules should compile to JavaScript that works fine with Gatsby.

There are a few issues with GraphQL, though.

Gatsby’s GraphQL has to be used with a tagged template, and BuckleScript doesn’t support bindings to tags. To use it, you need to resort to writing queries with raw JavaScript.

Even after you write the queries, then using the data returned by queries is a hassle. Because Reason is strongly typed, you have to write lots of types for each query (or use an “open” JS object type). This isn’t always a problem at first, but can quickly become tedious when you have a lot of queries. You have to manually check that your types are in sync with your queries and with your server’s types. If they get out of sync, then all the type-checking is useless.

What GraphQL PPX does

GraphQL PPX solves all of this for you. It allows you to write queries directly in your Reason code like this:

[%graphql {|
  query ExampleQuery {
    myQuery {
      myField
    }
  }
|}];
Enter fullscreen mode Exit fullscreen mode

This code is doing a lot of work for us. It generates:

  • All of the types associated with the data, which are in sync with the server.
  • Parser and serializer functions that make your data more idiomatic for Reason (mainly it will turn Nullable fields into the Reason option type).
  • The query itself, complete with the template-tag Gatsby needs to compile it.

The PPX also includes a lot more functionality, such as the ability to write custom parsers and custom types. But even at the minimum it’s saves a lot of work for you out of the box.

Refer to the official README to see how to install and set up the PPX.

Using the PPX with Gatsby

Gatsby’s rules for using queries are very specific and rigid. The PPX won’t follow them automatically, but, fortunately, it’s not hard to configure it.

Using tagged templates

By default, the PPX will output queries as plain strings. To use the Gatsby graphql tag, we need to add this to our bsconfig.json file:

{
  "ppx-flags": [
    [
      "@reasonml-community/graphql-ppx/ppx",
      "-template-tag-import=graphql",
      "-template-tag-location=gatsby"
    ]
  ]
}
Enter fullscreen mode Exit fullscreen mode

The two -template-tag-* arguments tell the PPX to import the graphql tag from the gatsby package.

Disabling the tag in the Node API

If you’re writing queries to be used in the Gatsby Node API, then the tag won’t work for those, and you need the queries to be regular strings. You can disable it for individual queries with the {taggedTemplate: false} configuration:

[%graphql {|
  query ExampleQuery {
    myQuery {
      myField
    }
  }
|};
  {taggedTemplate: false}
];
Enter fullscreen mode Exit fullscreen mode

Only use one query per file

Gatsby only allows one “root” query per JavaScript file. This can become a problem if you want to do things like define fragments to use in the same query. The solution right now is to extract the fragments to their own files.

For my site, I organized these in a src/Queries folder with Query_SiteMetadata.re, Query_Images.re, Query_Frag_ImageFixed.re, etc.

Make sure you directly export queries

In our query example above, the PPX will generate a module ExampleQuery which contains all of our types and functions. The query itself will exist as ExampleQuery.query. Reason exports everything by default, but here it will export the ExampleQuery module, not the query directly. This may seem like a subtle difference, but it means that Gatsby won’t “see” the query, since it doesn’t look deeply into the objects being exported.

This is easy to fix. One way is to export it manually.

let query = ExampleQuery.query;
Enter fullscreen mode Exit fullscreen mode

You can also “inline” the whole query.

[%graphql {|
  query ExampleQuery {
    myQuery {
      myField
    }
  }
|};
  {inline: true}
];
Enter fullscreen mode Exit fullscreen mode

{inline: true} will make the PPX generate all of its code inside the current module, instead of creating an ExampleQuery module for it.

You can also remove the query name, which has the same effect as {inline: true}.

[%graphql {|
  query {
    myQuery {
      myField
    }
  }
|}];
Enter fullscreen mode Exit fullscreen mode

Use useStaticQuery in the same file

If you’re using the useStaticQuery hook, it’s picky about its query input. The query has to be statically known by the Gatsby compiler, which means it can’t be a function argument and it can’t be an imported value. In short, you have to use it in the same file as the query itself.

/* ModuleName.re */
[%graphql {|
  query ExampleQuery {
    myQuery {
      myField
    }
  }
|}];

[@bs.module "gatsby"]
external useStaticQueryUnsafe: 'a => ExampleQuery.Raw.t = "useStaticQuery";
/* It's "unsafe" because the input isn't type-checked */

let useQuery = () =>
  ExampleQuery.query->useStaticQueryUnsafe->ExampleQuery.parse;
Enter fullscreen mode Exit fullscreen mode

Now you can use ModuleName.useQuery() in your components.

Reuse fragments to share types

Reason records are nominally typed, so two records with identical structures won’t be interchangeable. If you fetch identical data a lot across queries, you may need the PPX to reuse the same types instead of generating new ones.

The easiest way of doing this is with fragments. You can, for example, use a fragment to define image data and then use the types for that fragment in your image component. Any time you need an image, just use the fragment in your query.

Quality of life tips

There are a few things you can do that aren’t directly related to the PPX per se, but can make your work a lot easier.

De-nullify nullable fields

The PPX keeps its types in sync with your server. Some plugins, like gatsby-transformer-remark, will automatically infer and create types for data on your server, but usually make them nullable by default. For certain queries, that can mean a lot of switching or Belt.Option.maping to get the values you need, even if you know they’ll always exist.

Suppose you have markdown pages with YAML front-matter for metadata like title and date. Gatsby will automatically create those types, but will make title, data, and frontmatter itself, nullable (after all, Gatsby can’t know if they’ll always exist). You can force Gatsby to make them non-nullable with createSchemaCustomization.

For this markdown example, we would do something like this (with Reason syntax):

/* GatsbyNode.re */
type actions = {createTypes: (. string) => unit};
type t = {actions};

let createSchemaCustomization = ({actions: {createTypes}}) =>
  createTypes(.
    {|
    type MarkdownRemark implements Node {
      frontmatter: Frontmatter!
    }
    type Frontmatter {
      title: String!
      date: Date! @dateformat
    }
  |},
  );
Enter fullscreen mode Exit fullscreen mode

This will force those fields to be non-null. If you try creating a markdown page now without a title or a date, then the Gatsby compiler will raise an error for you.

(Don’t forget to refresh your graphql_schema.json file after you change the schema!)

Using Reason with both the Node API and the frontend API

If you’re using ES6 for the frontend but want to write your gatsby-node.js config with Reason (like we just did above), you’ll need to also compile to commonJS. One easy way to do that is in your bsconfig.json.

{
  "package-specs": [
    {
      "module": "es6",
      "in-source": true
    },
    {
      "module": "commonjs",
      "in-source": false
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The ES6 will generate in your src directory, and the commonJS will generate in lib/js.

Now you can put something like this in gatsby-node.js:

const config = require("./lib/js/GatsbyNode.bs.js");
exports.createSchemaCustomization = config.createSchemaCustomization;
Enter fullscreen mode Exit fullscreen mode

Conclusion

Once you get GraphQL PPX set up and running with your Gatsby site, the experience is amazing. It’s really the power of static typing at its best, since you have all of your business logic, UI components, and data type-checked by the same Reason compiler. You get the “if it compiles, it works” effect, and it’s hard to imagine building a website without it.

You can also check out Reason-Gatsby, which includes bindings to some of the basic Gatsby functions like Link and useStaticQuery.

On the other hand, the PPX is still rough around the edges. I wouldn’t recommend it to anyone who isn’t ready to get their hands dirty and potentially encounter bugs in the PPX itself.

On top of that, there’s still a lot of mental overhead required since you have to keep track of your Reason code, what the PPX is doing, and the Gatsby compiler’s rules all at the same time. There’s a lot of “magic” happening behind the scenes, which can make some errors hard to debug.

As for these downsides, I hope that they’ll minimize over time, especially as the GraphQL PPX becomes stable and emerges from beta. For the meantime, if you don’t mind tinkering and filing bug reports, then I highly recommend GraphQL PPX.

Top comments (1)

Collapse
 
kepi profile image
Kepi

I believe your article is only reason (no pun intended) why I had been able to finally get going with graphql-ppx in Gatsby.

Documentation for graphql-ppx is nice, but little too generic for noob as me. For beginner every example is gold and you gave so many. Thanks!

To help those struggling with rescript and current Gatsby, I had to remove template-tag-import:

"-template-tag-import=graphql",
Enter fullscreen mode Exit fullscreen mode

and use manual import in every file with graphql tag.

%%raw("import { graphql } from 'gatsby'")
Enter fullscreen mode Exit fullscreen mode