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
}
}
|}];
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 Reasonoption
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"
]
]
}
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}
];
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;
You can also “inline” the whole query.
[%graphql {|
query ExampleQuery {
myQuery {
myField
}
}
|};
{inline: true}
];
{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
}
}
|}];
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;
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.map
ing 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
}
|},
);
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
}
]
}
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;
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)
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
:and use manual import in every file with
graphql
tag.