We have several stories to tell when it comes to how we built
urql. Especially in an ecosystem where two other GraphQL clients already dominated, we had to prove that there's still space for a third opinion in the ecosystem.
It's as important sometimes to highlight how decisions were made, rather than talking about what specific decisions we made. When it comes to
urql we followed specific Guiding Principles when making decisions that helped us differentiate how our GraphQL client would shape up to be and keep sight of our north star.
Finding our guiding principles
From the start, we knew that we we had to approach building a GraphQL Client with fresh eyes and didn't let others' decisions influence ours. We wanted to find a novel approach to how GraphQL clients operate, yet differentiate ourselves by keeping our implementation versatile. However, with whatever we build we know that building a user base is the hardest problem and people may come from using other GraphQL Clients. Hence, the third component was to keep things intuitive.
Thus, the guiding principles that we've followed when developing it can roughly be described with three words: Novel, Versatile, Intuitive.
Novel, Versatile, Intuitive
We developed from first principles, kept the core of our implementation flexible and unopinionated (although we must always remain within spec), which meant we provide a lot of "extensions". But, we also decided that any addition or extension to the client must make it more intuitve rather than more complex.
These principles guided us while implementing new features, assessing requests to update parts of our API, and changing the client's core as needed. They've informed us when a prior behavior was unintuitive or wasn't in line with our ideals.
This overall approach, in my opinion, has consistently paid dividends for us.
An extensible, minimal architecture
The core library is minimal and sits in the middle of bindings to UI frameworks on the one side and extensions, which we called "exchanges," on the other. What people do with
urql is really up to which exchanges they leverage, we managed to decouple the core from any opinions. The reality is that few people have the same requirements. Not even normalized caching is essential to every developer using GraphQL.
The initial event hub client that is now
@urql/core just fell into place over time. The code did change, but the implementation has remained relatively similar.
What enabled us to end up at a point where
@urql/core implements just a single
Client interface and everything revolving around it is the realisation that GraphQL is extremely declarative. Queries we define can be updated reactively or time, and a GraphQL Client's responsibility is to manage this reactivity and abstract caching and the delivery of this data.
Fast & tiny normalized caching
What didn't just fall into place was Graphcache, the implementation of our normalized cache. It took a long time for all of it to come together, and we went through many iterations. (Its history is easily traceable because it used to live outside of the monorepo that
urql has today)
We started with GraphQL document traversal, ensured that it could handle each part of a document, wrote what read from a cache, what writes to the cache, what data structure everything should be in. We went through probably tens of refactors, bug fixes, and iterations on all of this.
I've got a write-up of how Graphcache generally works today, but there are many ways to implement a normalized cache (for GraphQL). There are choices around how prior queries update as the cache changes, how to do optimistic updates, handle network timing and many more decisions that can make or break a cache. Not only that, but all of these factors influence bundle size and performance.
If anyone was to build a GraphQL Client from scratch, there are myriads of decisions to make. However, I believe we've succeeded in balancing each decision against our guiding principles and fitting all of it into a tiny and fast package — not as in the JS buzzwords, but really.
That's what makes urql different.