From Apollo to Urql — Part 1

Daniel Zlotnik
Sesame Engineering
Published in
6 min readDec 7, 2021

Our notes at Sesame from migrating a highly active codebase from Apollo-client to Urql.

What this blog post is not about

The comparison between the two GraphQL clients and the motivation to pick one over the other have been discussed on countless occasions, however reading those before setting out on that path made us believe the migration was going to be a fairly smooth and quick process. It was not.

While Urql and Apollo share the same concepts and a close API, we discovered along the way a few complications which made this migration less than trivial.

What this blog post is about

Providing a clearer, more realistic picture of such a migration.

Everything you’re about to read is limited to the context of our project setup and needs so it is probably best to lay those out first.

Our setup

We’ve been using the fairly common combination of:

  • React
  • NextJS
  • Apollo-client
  • Styled-components

Our Motivation

As our code-base, as well as user & SEO bots traffic started to grow — improving page speed and integration tests coverage became a priority.

We targeted Apollo as an obstacle for improving both, mainly because of the following points:

  • Bundle-size
  • Testing
  • NextJS integration

Apollo-client’s bundle-size

Apollo client’s V3 bundle size is quite massive:

And has been growing steadily.

Most of it is due to advanced features and complex caching we didn’t really need.

Urql is more bare-bone, but provides separate packages such as cache exchanges and integrations for a more advanced usage.

In this modular approach you can use only what you need, which in our case saved over 30kb of gzipped JS shared by all pages.

Testing with Apollo

Mocking Apollo has proven non-intuitive, time consuming and an overall poor developer experience which has led to lower test coverage of more complex, container components.

If you have any experience in attempting such mocking — you know exactly what we’re talking about.

If not — it is enough to have a look at the countless github issues and some noteworthy comments regarding undefined data and the frustrating debugging experience on the common occasion where MockedProvider does not behave as expected, and just logs the obscure “No more mocked responses for the query” error message.

This topic will be discussed in more depth on the next part of the blog.

Apollo and NextJS Integration

Although there are examples and blog-posts by Apollo out there — integration with NextJS has never been a first-class citizen and we’ve had quite a few problems with it.

Especially when upgrading to NextJS 10 but that’s worth its own blog-post so let’s just keep it at that.

All of the above made it clear for us that it was worth looking for an alternative.

So Why Urql?

Looking at GraphQL clients alternatives, there is a handful of clients for React.

Apollo seems to be the most popular, followed closely by Relay.

Urql is the new kid on the block.

Since we wanted a more lightweight solution, in all points mentioned above Urql proved to be a great fit.

Urql’s bundle-size

Urql’s basic bundle size is about 4 times smaller than Apollo’s

Testing with Urql

There’s no MockedProvider non-debuggable black-box with Urql, but rather a fully controlled, easy to mock executeQuery (or mutation / subscription) function.

Urql and NextJS Integration

Urql provides the straightforward, easy to use next-urql package, as an integral part of the library.

Migrating gradually

Migrating a low-profile, low-activity project could have been done in a single, massive PR without worrying about conflicts.

The repo we were migrating however was our main consumer app.

We’re talking about 15–20 Front-end devs merging to master and adding new data-fetching components on a daily basis, hence switching our GraphQL client in one go was not an option without a significant code-freeze, which we couldn’t afford.

Our goal was to run this migration seamlessly, without interfering with the daily work of any of the front-end devs.

So how do you achieve such a migration on a relatively low foot-print?

Adapters

While Apollo’s and Urql’s APIs are quite similar — they’re not identical.

To keep the footprint low and to have the app able to run both Apollo and Urql, we decided to create adapters which would keep Apollo’s API but use Apollo or Urql under the hood (based on an environment variable flag).

useQuery

As you can see, there are a couple of differences:

  • Arguments — Apollo expects the query as the first argument and the options as the second, whereas Urql expects both in a single argument.
  • Result — Apollo returns a single results object whereas Urql returns a tuple.

Hence an adaptor from Urql to Apollo would look something like:

This way, migrating any usage of Apollo’s useQuery is as simple as changing the import:

import { useQuery } from '@apollo/client';
-->
import { useQuery } from 'hooks/useCustomQuery';

Options

Here Urql’s API is quite different, so it is better to only adapt the Apollo options you are actually using in your code, as that’s usually a small subset of the available options.

In our case we only needed to adapt:

  • Apollo’s skip to urql’s pause
  • Apollo’s ssr to urql’s context.suspense

However we did use some more advanced Apollo features like fetchMore (pagination) and polling which will be detailed in the next part of the blog.

useMutation

Both libraries have a useMutation hook but their arguments and return values differ.

Similar to useQuery, we had to create a useCustomMutation to even out some of the differences between Apollo and Urql.

Both provide a mutate function that will fire the mutation when executed and an OperationResult object (data, fetching/loading, error), however the order is reversed.

A small difference we had to account for.

When it comes to useMutation arguments, Apollo has a second parameter for options whereas Urql will only accept one argument for the GraphQL mutation.

Similarly, when executing the mutation, you’ll only be able to send the variables with Urql whereas Apollo lets you send options too.

Our common use case for mutation options was refetchQueries to bust the cache when applying a mutation.

Similar to mocking, we had to manage queries and variables to refetch which led more than once to some confusion.

Urql takes care of this out-of-the-box whether you’re using the document cache or normalized cache which removes a lot of overhead.

On this note, the complete implementation of useCustomMutation looks like this:

Note that the adapteduseMutation hook is compatible for both Apollo’s and Urql’s arguments whereas the adapted useQuery implementation is only compatible with Apollo’s.

Providers

Since we’re using NextJS — we used a custom HOC (which was using next-with-apollo internally) for Apollo, and next-urql’s withUrqlClient HOC for Urql.

withUrqlClient already wraps the given component with the provider whereas Apollo’s HOC passes the client as a prop.

So a wrapped NextApp compatible for both would look something like:

All of the above got us quite far with the migration, but as mentioned — there were a couple of more advanced Apollo usages which Urql (intentionally) does not cover.

More details about those, as well as migrating our tests and the outcome of the migration will be detailed in the second part of this blog.

--

--