Let's first analyze how to write the queries. I experimented the graphql_ppx
with a few different features (nullable and non-nullable variables, fragments, convert response into record automatically). The type safety is very neat, both the input variables and the response have specific types (no Js.Json.t
!), and the queries/mutations are validated against the schema! This makes it really easy to write queries, although I worry about integrating new versions of the schema since the repos are separated (we should probably re-fetch the schema as part of the CI).
As for the drawbacks, there are not many:
- The ppx really messes up my language server sometimes.
- I haven't tried a lot but I couldn't find easily a way to use refmt to format the queries.
- I couldn't find anything about custom directives, the lack of which would prevents us from using some features from the GraphQL clients (for example, managing local state using Apollo's
@client
directive). - I haven't hit problems writing the fragments and because of DDD we probably won't have many fragments anyway, but it's worth mentioning that its support is limited.
Overall, I think it's a really great option for writing the queries, much better than storing them as strings. We may run into some problems in the future if we require more complex features though.
It's trivial to imlement a GraphQL client using plain fetch
client due to helpers created by graphql_ppx
to serialize the query/variables and parse the response. It's not a bad option for starting, and by using React hooks it can be easily replaced. Of course if we need any more advanced features (such as automatically re-fetching queries after mutations, caching, batching, deduping, etc.), it's better to use a library.
Apollo was introduced as a response to Relay Classic, Facebook's GraphQL client which enforced a highly opinionated architecture, had a very complex setup and didn't have a lot of exchanges with the community. Apollo Client in the other hand doesn't make a lot of assumptions about your queries and it is very flexible. Its cache is based on normalizing objects (by default compounding id
and __typename
). There are multiple, composable parts to the Apollo environment (server, tracing, schema versioning, etc.), and it has a huge community. I has first-party support for Reason.
URQL was introduced as a response to Apollo's growing complexity of setup, which was mitigated afterwards with Apollo Boost (which comes with a default configuration without more advanced features, allowing for customization when needed). Its cache is based on hashing the entire query + variables, and it has first-party support for Reason.
Here follows a comparison between the two options:
--- | Apollo Client | URQL |
---|---|---|
Cache | Normalized objects | Hashing query + variables |
Batching | With apollo-link-batch-http (although it recommends deferring batching as long as possible) |
Doesn't have a first-party solution but allows to use Apollo's Link extensions |
Deduping | With apollo-link-dedup (enabled by default) |
With dedupExchange |
Authentication | Supports adding options to the fetch client or changing the network layer altogether |
Supports adding options to the fetch client or changing the network layer altogether |
Pagination | First-party support with fetchMore, also provides several recipes | No first-party support, needs to implement a custom solution |
React Hooks | No official support yet, seems to be waiting for Suspense to become stable (they recommend using react-apollo-hooks for now) | First-party support |
Optimistic update | mutate({ optimisticResponse }) (requires manipulating the cache if inserting new data) |
No support |
Local state | Support with @client directive |
No apparent support (I've found no mentions to this feature) |
Refetch after mutation | mutate({ refetchQueries }) |
Needs to manually call a function obtained when performing the query |
Subscriptions | Supports | Supports |
Community | Vibrant, easy to find answers online, official chat, huge number of issues and PRs | Almost non-existent |
Documentation | Very thorough, with several tutorials and recipes | Comprehensive |
Both options have first-party support for Reason, however not everything is implemented in neither.
Apollo has reason-apollo, which doesn't implement all features from Apollo Client. In particular I couldn't make the optimistic update work although it does have the optimisticResponse
as a labelled argument to the mutate
function. I get and error saying that I need to enable the addTypename
configuration because I have fragments in my query, but this option is not available to the client. I tried removing the fragments but then I need to manually add the __typename
to all queries. The refetchQueries
also doesn't work by passing existing queries, instead we need to pass the name of a query as a string, which is very error-prone. There's no update
argument to the mutate
function, which prevents us from manipulating the cache for optimistic updates that create new records. There's very limited third-party support for hooks with reason-apollo-hooks. Apollo also makes heavy use of custom directives for more advanced features, such as local state management, so due to the limitations of graphql_ppx
we wouldn't be able to make use of them. It's also worth mentioning that support for JSX3 is still in alpha.
URQL has reason-urql, which is also a bit limited. It doesn't implement the hooks present in the original package, and there's no working branch for supporting JSX3. There are less incompatibilies because URQL itself has less features than Apollo.
Apollo seems to me the clear winner. It's flexible and easy to setup, has advanced features, a great community and several other products in their platform. Considering that React Hooks was officially released in the beginning of February, it's kinda disappointing that there's no support yet, but that can be mitigated by using an external library (although it's very probable that the interface will be different).
The major drawback of adopting Apollo is the limited support for Reason. It's not possible to use several advanced features, and the reason-apollo
seems to move very slowly. Most of what's currently possible is not hard to implement using a plain fetch
. If we choose to use Apollo we'll probably need to write several custom bindings ourselves, and I'm really not sure what can be done about the custom directives issue.
That said, my opinion is that we should aim to wrap all querying/mutating logic in custom hooks anyway, so as long as it's possible to use the client with hooks, it shouldn't matter for the components that consume those hooks what's under the hood.