loading...
Cover image for GraphQL and Urql by Example
Open GraphQL

GraphQL and Urql by Example

deadcoder0904 profile image Akshay Kadam (A2K) ・8 min read

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.

Discussion

pic
Editor guide
Collapse
mikevocalz profile image
Mike Vocalz

Can you please post of screenshot of the demo?

Collapse
deadcoder0904 profile image
Akshay Kadam (A2K) Author

Hey Mike, there's actually nothing but <input>'s & <button>'s. Not really visually appealing so I went without screenshots otherwise I always put screenshots in every one of my posts.

You can actually find the whole code here → github.com/deadcoder0904/graphql-a...

Clone it & install the dependencies & run the project to see it yourself.

I've also added instructions on how to start the server-side in the server/ folder :)