Exploring urql from an Apollo perspective - LogRocket Blog

July 23, 2019 0 Comments

Exploring urql from an Apollo perspective - LogRocket Blog

 

 

I have been working with GraphQL at scale for over a year now, mainly with the Nordic subscription video-on-demand (SVOD) service C More, where the client data is being served from GraphQL. We have been using react-apollo on the web client, and seeing the GraphQL library urql pop up recently piqued my interest, particularly how it would compare to Apollo.

urql, which stands for Universal React Query Language, is  With ~2,500 weekly downloads to react-apollo’s ~500,000 as of July 2019, it doesn’t enjoy much use (yet), but the project has some alluring aspects. More on that later — first, I’d like to take a moment to reflect on why a library might be a good idea at all.

What is GraphQL, and why do we need a library?

GraphQL is a query language where the client asks the server for exactly what it needs — no more, no less. You can think of it as sending a string with all the keys of a JSON object that the server should populate for you. This is what a query can look like:

query { series(id: 3446) { title year suggestedEpisode { title episodeNumber } }
}

Which would return:

{ "data": { "series": { "title": "Game of Thrones", "year": 2019, "suggestedEpisode": { "title": "Winterfell", "episodeNumber": 1 } } }
}

GraphQL has three different operation types: query, mutation, and subscription. Query is for requesting data, mutation for changing data, and subscription for real-time data. Since I have limited experience with subscriptions, I will refrain from making a judgement on how urql handles it and focus on the more common operations: queries and mutations.

GraphQL queries and mutations are typically used over HTTP and often sent over POST requests (GET is generally supported, too). The GraphQL query is typically sent in the body of the request, together with any variables.

So why would you need a library to do this? To do simple things, you don’t — you can do simple fetch calls and it will work fine. It is my go-to way of using GraphQL if I’m calling it from a Node server or from a simple module with a few (rarely called) GraphQL requests. I feel it is often overlooked as an alternative for really simple use cases.

So what does a library give us? Well, GraphQL is using a type system for all data, which opens up for some client-side caching possibilities. That caching, together with some nice utilities around data fetching, is where a library will save you a lot of time. So let us take a look at how urql can save that time for us.

Using urql

As I mentioned earlier, urql is a lightweight, extensible GraphQL client for React. Its whole reason for being is to make GraphQL on the client side as simple as possible, as seen in the initial commit. That is immediately reflected in the installation; you just enter npm install urql graphql.

And then you do some minor setup:

import { Provider, createClient } from "urql" const client = createClient({ url: "http://localhost:1234/graphql", // you can also add more customizations here, // such as setting up an authorization header. // Advanced customizations are called "Exchanges", // and also go here if you need more advanced stuff.
}) ReactDOM.render( <Provider value={client}> <YourApp /> </Provider>, document.body
)

Now you are ready to use urql in your components!

Making a query

urql supports both a component API and a Hooks API. The component API is based on render props and consists of a <Query>, a <Mutation>, and a <Subscription> component. The most basic case looks like this:

function RenderPropVersion() { return ( <Query query={`{ # 4711 would normally be passed as a variable # (in all the following examples too, not just this one) movie(id: 4711) { title isInFavoriteList year } }`} > {({ fetching, data }) => fetching ? ( <div className="loader">Loading..</div> ) : ( <div className="json"> {JSON.stringify( data, null, 2 ) /* The (_, null, 2) makes JSON.stringify pretty. */} </div> ) } </Query> )
}

The Hooks API consists out of the useQuery, useMutation, and useSubscription Hooks, and the same component we have above looks like this with Hooks:

function HookVersion() { const [result] = useQuery({ query: `{ movie(id: 4711) { title isInFavoriteList year } }`, }) const { fetching, data } = result return fetching ? ( <div className="loader">Loading..</div> ) : ( <div className="json">{JSON.stringify(data, null, 2)}</div> )
}

Note how the hooks version has one less indentation level. As someone who has written components with sometimes three layers of <Query> and <Mutation> components with Apollo, let me just say that I love this. In the coming mutation section, you will be happy that the Hooks API exists.

When React renders an urql <Query> or useQuery, urql looks at the query and any variables and checks whether it has the result for that exact query cached. In that case, the result is immediately rendered. Otherwise, it sends off a request to populate the cache (this behavior can be modified with the requestPolicy prop/argument).

The urql cache

Since the main benefit you gain from a GraphQL library is the caching, I think it is important to have a decent mental model of how your library of choice handles that caching for you.

In urql, the result from queries are cached by the exact query (even the order of fields matter!) together with any variables, mapped to the result of that query. No magic is happening — it is a Map from input to output.

The cache is invalidated when data is changed via a mutation. When urql gets the mutation response back from the GraphQL server, urql looks at what types exist in the response. Any cached data containing those types will be invalidated, and any currently rendered queries that got their cache invalidated will refetch.

There is no manual access to the cache. The caching is done behind the scenes, all to make it easy for the user.

Mutating data

Mutating data with urql from an API perspective is pretty straightforward if you are familiar with querying data. The Hooks version from above, with two mutations added to it, looks something like this:

function HookVersion() { const [result] = useQuery({ query: `{ movie(id: 4711) { title isInFavoriteList year } }`, }) // Mutations added here! (imagine if this was render props 😰) const [addFavoriteResult, addFavorite] = useMutation(`mutation { addMovieToFavoriteList(id: 4711) { title } }`) const [removeFavoriteResult, removeFavorite] = useMutation(`mutation { removeMovieFromFavoriteList(id: 4711) { title } }`) const { fetching, data } = result // <button> added in render return fetching ? ( <div className="loader">Loading..</div> ) : ( <> <button onClick={() => { if (data.movie.isInFavoriteList) { removeFavorite() } else { addFavorite() } }} > {data.movie.isInFavoriteList ? "Remove favorite" : "Add favorite"} </button> <div className="json">{JSON.stringify(data, null, 2)}</div> </> )
}

Remember that the invalidation of the cache is based on what types are included in the mutation response. What this means for you as an urql user is that you have to be thoughtful of what your GraphQL server returns.

Imagine if the removeMovieFromFavoriteList mutation were to return the entire list of all movies marked as favorite. That might not seem too illogical, since you are effectively mutating the favorite list when marking a movie as favorite. However, that turns out to be a bad idea.

The reason it is a bad idea is that it causes a bug! The bug that would occur is illustrated in the following scenario: the user removes the last item in the list of favorites so that the user no longer has any movies marked as favorite. The mutation response (the list of favorites) would be an empty array.

An empty array does not include any types. That means that urql would not invalidate the proper query caches, and the data would be out of sync with the server.

That being said, it is always a good idea to return whatever is actually being mutated in your GraphQL queries regardless of what library you use. Apollo would get a stale cache, too, from the example above.

The better response in this case would be the movie that we marked as favorite. That way, the response will always include the type, and urql can invalidate the correct caches.

Differences between urql and Apollo

Apollo is probably the most well-known and popular GraphQL library today, and the library I have the most knowledge of. Therefore, it seems logical to continue with a brief comparison.

Philosophy

urql comes with a single package, compared to the five-plus you would need with react-apollo (however, you can use apollo-boost, which gives you a similar setup experience as urql).

The file size of the libraries also differ: 91kB + 35kB for apollo-boost + react-apollo vs. 21.5kB for urql (minified, checked with BundlePhobia). These differentiating facts reflect their guiding philosophies and goals.

urql is all about being lightweight and extensible, trusting the open source community to solve niche problems such as persisted queries, a request-size optimization where GraphQL queries are stored on the server, and only a hash is sent along for the ride. Apollo is a company, and it feels like they want to have a solution for every problem themselves.

Both are valid philosophies, but it could be valuable to think about when you are picking your library.

API

When evaluating the API, they look very similar. Create a client connected to your endpoint, hook it up to a <Provider>, and use queries, mutations, and subscriptions in your components.

Both libraries expose <Query>, <Mutation>, and <Subscription> render prop components to work with your API. urql also supports useQuery, useMutation, and useSubscription Hooks. Apollo has also created a Hooks API but not yet documented it.

Right now, React Suspense is not released yet, but we can be sure that both libraries will support it. Whether the API is different, or just a removal of the fetching state, is yet to be seen.

Apollo has a lot of API that urql does not. For example, Apollo gives you direct access to the cache. That can be really useful if you are working against a GraphQL schema that does not return the types needed for cache invalidation.

You can work around such problems in urql by (ab)using the requestPolicy argument/prop, but I would argue that it is nicer to work with such schemas with Apollo.

Caching

Caching is probably where Apollo and urql differ the most. Apollo normalizes its cache, meaning every item that is returned from GraphQL is cached by its id and its type. That combination is a decent heuristic since you can’t cache by id only (a User and Movie could potentially have the same id). Apollo also caches on a query level — if you are curious about how the cache looks, I’d suggest you download the Apollo devtools, where you can inspect the cache.

The normalized cache means that if you have the same item on the same page from two different queries, mutating one will mutate the other; they are both rendered from the normalized cache.

However, there is a tradeoff with Apollo’s caching. Imagine that we are displaying a list of movies marked as favorite, and another list of movies (new releases or similar) where each movie has a Mark as Favorite button with its current state of favoritedness (yes, that is a word now) visible on each movie.

If we were to click that button so that the movie changed its state of favoritedness, the GraphQL server would return the updated Movie with updated isInFavoriteList field. That would update the favoritedness state of the Movie, but the movie would not appear in the list of your favorite movies since the updated list was not part of the response.

That problem would not happen with the caching strategy of urql. As I said before, urql’s approach to caching is more simple: it caches on the query level, not each individual item. To make sure that the cache is not stale after mutations, it simply clears the cache of all queries that returned some item with the same type as the mutation returned.

The urql caching method might work well for some sites and not so well on others. For example, if C More (the Nordic streaming service, remember?) were to clear the cache of all Movie items if you used the mutations addMovieToFavoriteList or markAsSeen on a single Movie, it would basically empty the entire cache.

Conclusion

To be honest, I was surprised to see how similar urql and Apollo are. urql is simpler but lack some features out of the box, such as persisted queries. urql is also quite liberal with deleting things from the cache, so if you have a mutation-heavy, few-data-types application, the urql caching strategy might not be optimal for you.

However, since the Apollo and urql APIs are so similar, changing from urql to Apollo should not be very complicated. If and when you run into problems where you need the normalized cache or features such as persisted queries, you can take the rewrite without much cost.

So if you are looking for a simple library to get you going with GraphQL, I would absolutely recommend that you give urql a shot.

LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.


Tag cloud