According to the docs,
urql is a GraphQL client that exposes a set of React components and hooks. It's built to be highly customisable and versatile so you can take it from getting started with your first GraphQL project all the way to building complex apps and experimenting with GraphQL clients.
Urql, pronounced as Urkel
, recently reached v1.0 a few months ago. Unlike Apollo, it is a minimalistic GraphQL Client. Urql was introduced as a response to Apollo's growing complexity of setup.
Comparison between Apollo and Urql
A detailed comparison between Apollo and Urql can be found below (credits to this original gist, edited for recent corrections):
Features | Apollo Client | Urql |
---|---|---|
Cache | Normalized objects | Hashing query + variables. Normalized Cache is WIP |
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 | Officially supported as of v3.0 | First-party support |
Optimistic update |
mutate({ optimisticResponse }) (requires manipulating the cache if inserting new data) |
No support because of document based cache |
Local state | Support with @client directive |
No official support |
Refetch after mutation | mutate({ refetchQueries }) |
Needs to manually call a function obtained when performing the query |
Subscriptions | Supported | Supported |
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 |
Setting up the GraphQL server
A GraphQL server has been made with Prisma 2 specifically for the purpose of this tutorial so make sure you clone it.
After cloning it, install the dependencies using yarn
. This will also run the postinstall
hook which runs prisma2 generate
that generates the photon and nexus-prisma types inside node_modules/@generated
folder.
Go ahead and start the server using:
$ yarn start
Open up http://localhost:4000 to play around with the GraphQL API.
Getting Started with Urql
Install urql
with the package manager of your choice:
$ yarn add urql
# or
$ npm install urql
urql
has a Provider
component similar to other libraries like react-redux
which manages state and data. You need to wrap your app with the Provider
component. This <Provider>
component holds the client
that is used to manage data, requests, the cache, and other things such that every component below it has an access to the client and it can query or mutate the data.
import React from 'react';
import { Provider, createClient } from 'urql';
const client = createClient({
url: "http://localhost:4000"
});
const App = () => (
<Provider value={client}>
{/* ... */}
</Provider>
);
export default App;
Querying data in Urql using Render Props or React Hooks
Let us query some GraphQL data using urql
's Query
component.
import React from 'react';
import { useQuery } from "urql";
const getPokemonData = `
query GetPokemonData($name: String!) {
pokemon(name: $name) {
id
number
name
attacks {
special {
id
name
damage
}
}
}
}
`;
export const ListPokemonDataQuery = ({ name = "Pikachu" }) => {
const [{ fetching, data, error }] = useQuery({
query: getPokemonData,
variables: { name }
});
if (fetching) {
return `Loading ${name}...`;
} else if (error) {
return `Oh no! Error: ${error}`;
}
const pokemon = data.pokemon[0];
return (
<>
<h1>
#{pokemon.number} {pokemon.name}
</h1>
<ul>
{pokemon.attacks.special.map(({ name, id, damage }) => (
<li key={name}>
#{id} {name} - {damage}
</li>
))}
</ul>
</>
);
};
The above Query
component sends getPokemonData
query with name
as a variable to the GraphQL API mentioned in url
property of createClient
.
Query
is a render prop which is nothing but a React component whose value is a function. This render prop gives us fetching
, data
and error
. fetching
returns a boolean whether the request is still being sent and is still loading. data
gives us the data returned by the GraphQL API and error
gives us whether we have any errors with the GraphQL API.
urql
also has a first-class Hooks support so we can also use useQuery
function.
If we rewrite the above example, it would look like:
import React from "react";
import { useQuery } from "urql";
const getPokemonData = `
query GetPokemonData($name: String!) {
pokemon(name: $name) {
id
number
name
attacks {
special {
id
name
damage
}
}
}
}
`;
export const ListPokemonDataHook = ({ name = "Pikachu" }) => {
const [{ fetching, data, error }] = useQuery({
query: getPokemonData,
variables: { name },
})
if (fetching) {
return `Loading ${name}...`;
} else if (error) {
return `Oh no! Error: ${error}`;
}
const pokemon = data.pokemon[0];
return (
<>
<h1>
#{pokemon.number} {pokemon.name}
</h1>
<ul>
{pokemon.attacks.special.map(({ name, id, damage }) => (
<li key={name}>
#{id} {name} - {damage}
</li>
))}
</ul>
</>
);
}
Notice, how the useQuery
hook simplifies the component structure. useQuery
works like any other React Hook as it takes in a value and returns a tuple. The value it takes in is a query and a variable name and it returns back a tuple containing fetching
, data
and error
. Everything else is just the same.
Mutating data in Urql using Render Props or React Hooks
Let us mutate some GraphQL data using urql
's Mutation
component.
import React, { useState } from 'react';
import { Mutation } from 'urql';
const addPokemon = `
mutation AddPokemon($number: Int!, $name: String!) {
addPokemon(data: {
number: $number,
name: $name
}) {
id
number
name
}
}
`
export const InsertPokemonMutation = () => {
const [name, setName] = useState('')
return (
<Mutation query={addPokemon}>
{({ fetching, data, error, executeMutation }) => {
return (
<>
{error && <div>Error: {JSON.stringify(error)}</div>}
<input value={name} onChange={e => setName(e.target.value)} />
<button onClick={() => {
if (name.trim() === "") return // return if input is empty
executeMutation({ name, number: Math.ceil(Math.random() * 1000) })
setName("") // clear the input
}}>
Add Pokemon
</button>
{data && (<div>
<br/>
Mutation successful:
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>)}
</>
)
}}
</Mutation>
)
}
Mutation
component takes in a query and returns executeMutation
. executeMutation
is a function which takes in a variable name
and a random number
as stated in our addPokemon
query above and calls the Mutation
. If the mutation is unsuccessful then an error
is displayed. The render prop also gives you fetching
and data
if you want to do anything with it.
If we rewrite the above example using useMutation
hook then it would look like:
import React, { useState } from 'react';
import { useMutation } from 'urql';
const addPokemon = `
mutation AddPokemon($number: Int!, $name: String!) {
addPokemon(data: {
number: $number,
name: $name
}) {
id
number
name
}
}
`
export const InsertPokemonHook = () => {
const [name, setName] = useState('')
const [{ fetching, data, error }, executeMutation] = useMutation(addPokemon)
return (
<>
{error && <div>Error: {JSON.stringify(error)}</div>}
<input value={name} onChange={e => setName(e.target.value)} />
<button onClick={() => {
if (name.trim() === "") return
executeMutation({ name, number: Math.ceil(Math.random() * 1000) })
setName("")
}}>
Add Pokemon
</button>
{data && (<div>
<br/>
Mutation successful:
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>)}
</>
)
}
useMutation
takes in the mutation addPokemon
and returns the mutations state (fetching
, data
and error
) and executeMutation
function in a tuple. executeMutation
is then called on a click of the button.
What are Exchanges in Urql
urql
has a concept of exchanges
. When a new Client()
is created you pass it a url
and fetchOptions
. But you can also pass it an exchanges
array. Exchanges are operation handlers. It receives client
and forward
function as an object and returns a function accepting a stream of operations and returning a stream of operation results (i.e. GraphQL results).
In other words, exchanges are handlers that fulfill our GraphQL requests. They're Input/Output streams, inputs being operations, outputs being results.
By default, urql
creates 3 different exchanges namely dedupExchange
, cacheExchange
and fetchExchange
.
dedupExchange
deduplicates pending operations. It eliminates duplicate operations waiting for a response as it wouldn't make sense to send the same operation twice at the same time.
cacheExchange
checks operations against the cache. Depending on the requestPolicy
cached results can be resolved instead and results from network requests are cached.
fetchExchange
sends an operation to the API and returns results.
When a new Client()
is created and no exchanges are passed to it then some are added automatically, which is the same as creating a new Client()
using the following exchanges:
import { Client, dedupExchange, cacheExchange, fetchExchange } from "urql";
const client = new Client({
url: "http://localhost:4000",
exchanges: [dedupExchange, cacheExchange, fetchExchange]
});
This can also be written as:
import { Client, defaultExchanges } from "urql";
const client = new Client({
url: "http://localhost:4000",
exchanges: defaultExchanges
});
Now that we know what are exchanges, lets learn about subscriptions.
Subscribing to data in Urql using Render Props or React Hooks
Go ahead and first install subscriptions-transport-ws
using yarn
:
$ yarn add subscriptions-transport-ws
To use subscriptions, we need to first add subscriptionExchange
to our new Client()
and also create a new SubscriptionClient()
using websocket protocol as follows:
import { SubscriptionClient } from "subscriptions-transport-ws";
import { Client, defaultExchanges, subscriptionExchange } from "urql";
const subscriptionClient = new SubscriptionClient(
"ws://localhost:4001/graphql",
{
reconnect: true,
timeout: 20000
}
);
const client = new Client({
url: "http://localhost:4000",
exchanges: [
...defaultExchanges,
subscriptionExchange({
forwardSubscription: operation => subscriptionClient.request(operation)
})
]
});
Now we can start using Subscription
component in our App:
import React from 'react'
import { Subscription } from 'urql'
const newPokemon = `
subscription PokemonSub {
newPokemon {
id
number
name
attacks {
special {
name
type
damage
}
}
}
}
`
const NewPokemon = () => (
<Subscription query={newPokemon}>
{({ fetching, data, error }) => {
if (fetching) {
return `Loading...`
} else if (error) {
return `Oh no! Error: ${error}`
}
const { newPokemon } = data
return (
<>
<h1>
#{newPokemon.number} {newPokemon.name}
</h1>
<ul>
{newPokemon.attacks.special.map(({ name, type, damage }) => (
<li key={name}>
{name} ({type}) - {damage}
</li>
))}
</ul>
</>
)
}}
</Subscription>
)
Subscription
component works in similar way to the Query
component. It can take in a query
and a variables
prop. It also has fetching
, data
and error
just like a Query
component. The data
and error
of the render props will change every time a new event is received by the server.
We can also use useSubscription
hook as follows:
import React from 'react';
import { useSubscription } from 'urql';
const newPokemon = `
subscription PokemonSub {
newPokemon {
id
number
name
attacks {
special {
name
type
damage
}
}
}
}
`
export const NewPokemonSubscriptionHook = () => {
const [{ fetching, data, error }] = useSubscription({ query: newPokemon }, (pokemons = [], res) => {
return [res.newPokemon, ...pokemons]
})
if (fetching) {
return `Loading...`
} else if (error) {
return `Oh no! Error: ${error}`
}
return (
<>
{data.map(pokemon => {
const { newPokemon } = pokemon
return (
<div key={newPokemon.number}>
<h1>
#{newPokemon.number} {newPokemon.name}
</h1>
<ul>
{newPokemon.attacks.special.map(({ name, type, damage }) => (
<li key={name}>
{name} ({type}) - {damage}
</li>
))}
</ul>
</div>
)
})}
</>
)
}
useSubscription
takes in the subscription newPokemon
and returns the subscriptions state (fetching
, data
and error
). Additionally, the second argument for useSubscription
can be an optional reducer function which works like Array.prototype.reduce. It receives the previous set of data that this function has returned or undefined
. As the second argument, it receives the event that has come in from the subscription. You can use this to accumulate the data over time, which is useful for a list for example.
Conclusion
In this tutorial, we learnt about URQL (Universal React Query Library) which is a blazing-fast GraphQL client, exposed as a set of ReactJS components. We then laid out the differences between Apollo and Urql.
We learnt about the Query
API, Mutation
API and Subscription
API provided by Urql. We also used the hooks useQuery
, useMutation
and useSubscription
to reduce the callback hell boilerplate unnecessarily created by Render Props.
We also learnt about Exchanges. Finally, we created a simple Pokemon application using Urql. Urql is a new piece of technology but it is mature enough to be used in production. Although, some things like Optimistic Updates do not yet work due to lack of cache normalization but its a work-in-progress and soon will be released.