Overview
What Even Is This?
This project aims to provide a hassle-free way of using Apollo's React library in your ReasonML project. To that end, it includes:
- A codegen plugin for generating ReasonML types that correspond 1:1 with your GraphQL schema's types.
- A codegen plugin for generating ReasonML types for your project's GraphQL operations' variables.
- ReasonML bindings for Apollo's react-hooks library that work hand-in-hand with the generated types, allowing you to write type-safe queries and mutations that use Apollo's fantastic tooling under the covers.
Is it ready for use?
Yes? Yes! Well, probably. All of the bits and pieces of this approach have been dogfooded while building a react-native + web app in ReasonML for My Well Ministry and it's been working great. That said, it's not been widely tested against they myriad of GraphQL schemas out there, and there are a few lesser-used parts of the Apollo API that aren't fully implemented yet. So, if you bump into something that isn't working for you, please go ahead and file an issue!
How is this different from other Reason + Apollo bindings?
The main difference between this project and most (all?) other approaches to ReasonML bindings for Apollo is that it does not utilize graphql-ppx to generate type code for your operations, opting instead to generate types for your entire schema in one go. You can read more about the difference (and the pros and cons of both approaches) in this blog post.
How does it work?
After setting your project up for using the codegen plugins and running it against your schema, you'll wind up with two generated Reason files:
- Abstract types that correspond 1:1 with all of the types defined in your GraphQL schema.
- Modules corresponding to each query + mutation in your app that tie in with the Apollo bindings to type the operation variables and pull in the operation's ast document for you.
So, if you've got an operation defined like so:
query MyCoolQuery($filter: String!) {
todos(filter: $filter) {
id
title
isComplete
}
}
You can use it in a reason-react component like this:
[@react.component]
let make = () => {
open Apollo.Queries.MyCoolQuery;
let variables = makeVariables(~filter: "all", ());
let response = useQuery(~variables, ());
switch (response) {
| {loading: true} => <LoadingIndicator />
| {error: Some(err)} => <ErrorDisplay err />
| {data: Some(queryRoot)} =>
let todos = queryRoot->Graphql.Query.todos;
<TodosList todos />
};
}
Notice how the Apollo hooks code looks almost exactly like its JS counterpart, but we've got typed variables + the ability to pattern match on the response 😍!
The first thing to notice is that we're opening the Apollo.Queries.MyCoolQuery
module. That's code that was all generated for you automatically, just based on your GraphQL operation. It contains the makeVariables
creation function, as well as the useQuery
hook which is specifically typed for this operations' variables.
Next, take a look at how we're working with the response from useQuery
. Just like in the JS counterpart, we can access loading
, error
, and data
fields, only they're typed as a Reason record, and with option
where appropriate instead of undefined
.
Finally, look at how we're using the query's returned data. queryRoot
is an abstract type representing the root Query
type in your schema. All GraphQL queries always return this type. We can pass this type to a getter function named for the field we want: todos
. The result is a an array of the Todo
type. The TodosList component's type signature can specify it wants array(Graphql.Todo.t)
. Maybe there's another part of your app where you show a list of todos, but fetched with a slightly different query - no worries, you can still reuse the same component there as well!
Handling Unfetched Fields
Using abstract types in this way, rather than generating a type that corresponds directly to the fields we queried for does have a drawback: there's no way to ensure we're not trying to use a field we didn't actually fetch at compile time. For example, if my <TodosList>
compomnent depends on a createdAt
field (which wasn't fetched in the example query), there's no way for the compiler to catch my error and tell me.
However, we can still check for this error at run time, and that's exactly what the abstract type getter functions do. If your code tries to get a field that isn't present in the underlying JSON object, you'll see a rumtime error thrown that helpfully tells you which field is missing on which type, allowing you to catch your error while still in the development cycle well before shipping the bad code to users!