When picking a GraphQL client for React, many default to using Apollo or Relay, but now there’s a new kid on the block rising in popularity over the last year: Its name is urql
. It's not as packed with features as other GraphQL clients. Instead, urql
aims to be minimal and highly customizable. This blog post series will start by walking you through getting started with urql
, and then move on to more advanced topics like subscriptions, normalised caching, etc.
Concepts
This blog series assumes a basic understanding of GraphQL. The following basic urql
concepts will also be referenced throughout the series.
Operations
In urql
all operations are controlled by a central client. This client is responsible for managing GraphQL operations and sending requests. This includes things like queries, mutations, and subscriptions.
A typical Request Operation
looks like this:
{ key: 'hash', operationName: 'query | mutation | subscription | teardown', variables: {}, context: { fetchOptions: 'function | object', requestPolicy: 'cache-first | cache-only | network-only | cache-and-network', url: 'string' } }
The most important properties are listed in the above example; more properties can be found here.
The above key
property is a hash of the querystring
+ variables
used for this operation. This key
uniquely identifies every operation, so, if we have two components dispatching the same query with the same variables, we can purposely ignore one of them to avoid duplicate requests.
With the requestPolicy
we can dictate whether or not we want to use our cache, and whether or not we want to fetch even if there is a cache-hit.fetchOptions
allows us to dictate what headers and other options to use with the fetch
action.
When an Operation comes back as a cache-hit
or as a fetched result we start calling it an OperationResult.
This will typically look like this:
{ operation, // the operationRequest mentioned earlier errors, // our possible server response errors data, // the data received extensions // possible extensions attached to the response by your backend }
An OperationResult
will then be handled by exchanges before reaching the client.
Exchanges
Exchanges are middleware-like extensions that handle how operations flow through the client and how they're fulfilled. Multiple exchanges may handle each operation.
You can pass in these exchanges to the client like this:
createClient({ exchanges: [exchange1, exchange2, ...] });
The exchanges will be executed in the order provided to the client. This means that when an operation comes in, exchange1
will be called. When exchange1
is done, the operation gets forwarded to exchange2
and so on. When the last exchange completes, we get an OperationResult
. This OperationResult
is then sent back through the chain of exchanges in the reverse direction, and finally reaches the client.
More info around exchanges can be found here.
__Typename
Every type we make in our graphql-server
will have a name and send it back when we query the __typename
field. For example, the entity below will implicitly have an additional __typename: 'Todo'
field.
type Todo { id: ID! text: String completed: Boolean }
The __typename
field is useful for identifying the queries affected by a certain mutation
. When a mutation
receives a response with a __typename
we're currently watching with a query, then we can assume this watched query should be invalidated.
Getting started
If you want to follow along you can use this template.
For this walkthrough we'll be using React.js but note that urql can be used outside of React.
Starting out with urql
is pretty convenient. First, we create our client. This client will process the operations and their results.
// App.js import { createClient } from 'urql'; const client = createClient({ // This url can be used in your sandbox as well. url: 'https://0ufyz.sse.codesandbox.io', });
The client has more options, but the url is the only mandatory one. A few exchanges are included by default:
Find more client-options here.
Next, set up a Provider
to allow our React-tree to access the client.
import { createClient, Provider } from 'urql'; const client = createClient(...); export const App = () => ( <Provider value={client}><Todos /></Provider> );
At this point, our client is set up to handle incoming results, and our App
has access to this client and can dispatch operations. The only thing we are still missing is actually dispatching operations, so let's make our first query:
import { useQuery } from 'urql'; const TodosQuery = ` query { todos { id text complete } } `; export const Todos = () => { const [result] = useQuery({ query: TodosQuery }); if (result.fetching) return <p>Loading...</p>; if (result.error) return <p>Oh no... {result.error.message}</p>; return ( <ul> {result.data.todos.map(({ id, text, complete }) => ( <Todo key={id} text={text} id={id} complete={complete} disabled={result.fetching} />) )} </ul> ); }
In the example above, if todo results are present in the cache they will be returned synchronously (no result.fetching
) and if they're not they will be fetched.
More options for the useQuery hook can be found here.
You might worry that this architecture would result in unnecessary fetching, but the first default exchange included in your urql-client
is the dedupExchange
. Do you recall us talking about a unique key on each operation? We use that key to determine in that dedupExchange
whether or not we already have an operation in progress for a given piece of data. When queries and variables are identical, a new fetch is not performed.
We are still missing one crucial part: we want to be able to mark a todo as completed. Let's refactor our application to allow each Todo
item to toggle and persist its completed state.
import { useMutation } from 'urql'; const ToggleTodoMutation = ` mutation($id: ID!) { toggleTodo(id: $id) { id } } `; export const Todo = ({ id, text, complete, disabled }) => { const [result, toggleTodo] = useMutation(ToggleTodoMutation); if (result.error) return <p>Something went wrong while toggling</p>; return ( <li> <p onClick={() => toggleTodo({ id })}> {text} </p> <p>{complete ? 'Completed' : 'Todo'}</p> <button onClick={() => toggleTodo({ id })} disabled={disabled || result.fetching} type="button" > {complete ? 'Toggle todo' : 'Complete todo'}</button> </li> ); }
Notice the disabled={result.fetching}
on our Todo
component. Our example uses a document-based cache, so when we do a mutation on a certain __typename
, queries associated with this type will be refetched. In our case, toggling the completed state of our Todo
type will cause our todos
query will be refetched, so we prevent additional toggles while the result is fetching.
Try opening the network-tab of your browser when this mutation
completes. You'll see a query being triggered to refetch our todos
. This is because our cacheExchange
sees a mutation response with the typename "Todo"; it knows that we are currently watching an array of this type and invalidates it, triggering the refetch.
If you'd like to dig into exactly how caching and the dedupExchange
is working, you can delay the mounting of this second component until the first has fetched. You will see the data for the query return synchronously, thanks to our cacheExchange
. The default cache will save responses by their operation key.
You can also try altering the default caching behavior by changing the requestPolicy
from the default cache-first
to cache-and-network
. This will force the query to refetch in the background.
More options for the useMutation hook can be found here.
Conclusion
This was an introduction to urql
, the new kid on the block for GraphQL clients. In the future we'll cover how to setup subscriptions, server-side rendering, and more.
We hope you learned something and are as excited as we are about this new library! Continue reading for How to urql, Part 2: Authentication & Multiple Users.