/urql

The highly customizable and versatile GraphQL client.

Primary LanguageTypeScriptMIT LicenseMIT

urql

A highly customisable and versatile GraphQL client

Test Status Maintenance Status Spectrum badge

✨ Features

  • 📦 One package to get a working GraphQL client in React or Preact
  • ⚙️ Fully customisable behaviour via "exchanges"
  • 🗂 Logical but simple default behaviour and document caching
  • ⚛️ Minimal React components and hooks
  • 🌱 Normalized caching via @urql/exchange-graphcache

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.

While GraphQL is an elegant protocol and schema language, client libraries today typically come with large API footprints. We aim to create something more lightweight instead.

Some of the available exchanges that extend urql are listed below in the "Ecosystem" list including a normalized cache and a Chrome devtools extension.

The documentation contains everything you need to know about urql

You can find the raw markdown files inside this repository's docs folder.

🏎️ Intro & Showcase

Installation

yarn add urql graphql
# or
npm install --save urql graphql

Queries

There are three hooks, one for each possible GraphQL operation.

The useQuery hook is used to send GraphQL queries and will provide GraphQL results from your API.

When you're using useQuery it'll accept a configuration object that may contain keys for query and variables. The query can either be your GraphQL query as a string or as a DocumentNode, which may be parsed using graphql-tag for instance.

import { useQuery } from 'urql';

const YourComponent = () => {
  const [result] = useQuery({
    query: `{ todos { id } }`,
  });

  if (result.error) return <Error message={result.error.message} />;
  if (result.fetching) return <Loading />;

  return <List data={result.data.todos} />;
};

Internally, urql will create a unique key for any operation it starts which is a hash of query and variables. The internal "Exchange pipeline" is then responsible for fulfilling the operation.

Diagram: An Operation key is computed by hashing the combination of the stringified query and the stabily stringified variables. DocumentNodes may either be stringified fully or just by using their operation names. Properties of any variables object need to be stabily sorted.

The result's error is a CombinedError, which normalises GraphQL errors and Network errors by combining them into one wrapping class.

Diagram: A CombinedError has two states. It can either have a property 'networkError', or it can have multiple, rehydrated GraphQL errors on the 'graphQLErrors' property. The message of the CombinedError will always be a summary of the errors it contains.

Learn more about useQuery in the Getting Started guide

Mutations

The useMutation hook is very similar to the useQuery hook, but instead of sending queries it sends mutations whenever the executeMutation method is called with the variables for the mutation.

import { useMutation } from 'urql';

const YourComponent = () => {
  const [result, executeMutation] = useMutation(
    `mutation AddTodo($text: String!) { addTodo(text: $text) { id } }`
  );

  const add = () =>
    executeMutation({ text: 'New todo!' }).then(result => {
      /* ... */
    });

  return <button onClick={add}>Go!</button>;
};

The useMutation hook provides a result, just like useQuery does, but it doesn't execute the mutation automatically. Instead it starts once the executeMutation function is called with some variables. This also returns a promise that resolves to the result as well.

Learn more about useMutation in the Getting Started guide

Pausing and Request Policies

The useQuery hook and useMutation hook differ by when their operations execute by default. Mutations will only execute once the executeMutation method is called with some variables. The useQuery hook can actually be used similarly. The hook also provides an executeQuery function that can be called imperatively to change what query the hook is running.

Unlike the useMutation hook, the useQuery's executeQuery function accepts an OperationContext as the argument, this allows you to for example override the requestPolicy or even the fetchOptions.

const [result, executeQuery] = useQuery({
  query: 'query ($sort: Sorting!) { todos(sort: $sort) { text } }',
  variables: { sort: 'by-date' },
});

// executeQuery can trigger queries and override options
const update = () => executeQuery({ requestPolicy: 'network-only' });

Instead of running the useQuery operation eagerly you may also pass pause: true, which causes the hook not to run your query automatically until pause becomes false or until executeQuery is called manually.

// This won't execute automatically...
const [result, executeQuery] = useQuery({
  query: '{ todos { text } }',
  pause: true,
});

// ...but it can still be triggered programmatically
const execute = () => executeQuery();

Apart from pause you may also pass a requestPolicy option that changes how the cache treats your data. By default this option will be set to "cache-first" which will give you cached data when it's available, but it can also be set to "network-only" which skips the cache entirely and refetches. Another option is "cache-and-network" which may give you cached data but then refetches in the background.

const [result, executeQuery] = useQuery({
  query: '{ todos { text } }',
  // Refetch up-to-date data in the background
  requestPolicy: 'cache-and-network',
});

// this will tell you whether something is fetching in the background
result.stale; // true

Therefore to refetch data for your useQuery hook, you can call executeQuery with the network-only request policy.

const [result, executeQuery] = useQuery({
  query: '{ todos { text } }',
});

// We change the requestPolicy to bypass the cache just this once
const refetch = () => executeQuery({ requestPolicy: 'network-only' });

Learn more about request policies in our Getting Started section!

Client and Exchanges

In urql all operations are controlled by a central Client. This client is responsible for managing GraphQL operations and sending requests.

Diagram: The Client is an event hub on which operations may be dispatched by hooks. This creates an input stream (displayed as operations A, B, and C). Each Operation Result that then comes back from the client corresponds to one operation that has been sent to the client. This is the output stream of results (displayed as results A, B, and C)

Any hook in urql dispatches its operation on the client (A, B, C) which will be handled by the client on a single stream of inputs. As responses come back from the cache or your GraphQL API one or more results are dispatched on an output stream that correspond to the operations, which update the hooks.

Hence the client can be seen as an event hub. Operations are sent to the client, which executes them and sends back a result. A special teardown-event is issued when a hook unmounts or updates to a different operation.

Diagram: Operations can be seen as signals. Operations with an 'operationName' of query, mutation, or subscription start a query of the given DocumentNode operation. The same operation with an 'operationName' of 'teardown' instructs the client to stop or cancel an ongoing operation of the same key. Operation Results carry the original operation on an 'operation' property, which means they can be identified by reading the key of this operation.

Learn more about the shape of operations and results in our Architecture section!

Exchanges are separate middleware-like extensions that determine how operations flow through the client and how they're fulfilled. All functionality in urql can be customised by changing the client's exchanges or by writing a custom one.

Exchanges are named as such because middleware are often associated with a single stream of inputs, like Express' per-request handlers and middleware, which imperatively send results, or Redux's middleware, which only deal with actions.

Instead Exchanges are nested and deal with two streams, the input stream of operations and the output stream of results, where the stream of operations go through a pipeline like an intersection in an arbitrary order.

Diagram: By default the client has three exchanges. Operations flow through a 'dedup', 'cache', and 'fetch' exchange in this exact order. Their results are flowing backwards through this same chain of exchanges. The 'dedupExchange' deduplicates ongoing operations by their key. The 'cacheExchange' caches results and retrieves them by the operations' keys. The 'fetchExchange' sends operations to a GraphQL API and supports cancellation.

By default there are three exchanges. The dedupExchange deduplicates operations with the same key, the cache exchange handles caching and has a "document" strategy by default, and the fetchExchange is typically the last exchange and sends operations to a GraphQL API.

There are also other exchanges, both built into urql and as separate packages, that can be used to add more functionality, like the subscriptionExchange for instance.

Learn more about Exchanges and how to write them in our Guides section!

Document Caching

The default cache in urql works like a document or page cache, for example like a browser would cache pages. With this default cacheExchange results are cached by the operation key that requested them. This means that each unique operation can have exactly one cached result.

These results are aggressively invalidated. Whenever you send a mutation, each result that contains __typenames that also occur in the mutation result is invalidated.

Diagram: First, a query is made that gets a type, in this example a 'Book'. The result contains the '__typename' field that says 'Book'. This is stored in a mapping of all types to the operations that contained this type. Later a mutation may change some data and will have overlapping types, in this example a 'Book' is liked. The mutation also contains a 'Book' so it retrieves the original operation that was getting a 'Book' and reexecutes and invalidates it.

Normalized Caching

You can opt into having a fully normalized cache by using the @urql/exchange-graphcache package. The normalized cache is a cache that stores every separate entity in a big graph. Therefore multiple separate queries, subscriptions, and mutations can update each other, if they contain overlapping data with the same type and ID.

Diagram: A normalized cache contains a graph of different nodes. Queries point to different nodes, which point to other nodes, and so on and so forth. Nodes may be reused and are called 'entities'. Each entity corresponds to an object that came back from the API.

Getting started with Graphcache is easy and is as simple as installing it and adding it to your client. Afterwards it comes with a lot of ways to configure it so that less requests need to be sent to your API. For instance, you can set up mutations to update unrelated queries in your cache or have optimistic updates.

import { createClient, dedupExchange, fetchExchange } from 'urql';
import { cacheExchange } from '@urql/exchange-graphcache';

const client = createClient({
  url: 'http://localhost:1234/graphql',
  exchanges: [
    dedupExchange,
    // Replace the default cacheExchange with the new one
    cacheExchange({
      /* config */
    }),
    fetchExchange,
  ],
});

urql's normalized cache is a little different than ones that you may find in other GraphQL client libraries. It focuses on doing the right thing and being intuitive whenever possible, hence it has a lot of warnings that may be logged during development that tell you what may be going wrong at any given point in time.

It also supports "schema awareness". By adding introspected schema data it becomes able to deliver safe, partial GraphQL results entirely from cache and to match fragments to interfaces deterministically.

Read more about Graphcache on its repository!

Server-side Rendering

urql supports server-side rendering via its suspense mode and its ssrExchange. When setting up SSR you will need to set suspense: true on the Client for the server-side and add an ssrExchange.

import {
  Client,
  dedupExchange,
  cacheExchange,
  fetchExchange,
  ssrExchange,
} from 'urql';

// Depending on your build process you may want to use one of these checks:
const isServer = typeof window !== 'object' || process.browser;

const ssrCache = ssrExchange({ isClient: !isServer });

const client = new Client({
  suspense: isServer
  exchanges: [
    dedupExchange,
    cacheExchange,
    // Add this after the cacheExchange but before fetchExchange:
    ssrCache,
    fetchExchange
  ]
});

The ssrExchange is another small cache that stores full results temporarily. On the server you may call ssrCache.extractData() to get the serialisable data for the server's SSR data, while on the client you can call ssrCache.restoreData(...) to restore the server's SSR data.

Read more about SSR in our Basics' SSR section!

Client-side Suspense

You may also activate the Client's suspense mode on the client-side and use React Suspense for data loading in your entire app! This requires you to use the @urql/exchange-suspense package.

import { Client, dedupExchange, cacheExchange, fetchExchange } from 'urql';

import { suspenseExchange } from '@urql/exchange-suspense';

const client = new Client({
  url: 'http://localhost:1234/graphql',
  suspense: true, // Enable suspense mode
  exchanges: [
    dedupExchange,
    suspenseExchange, // Add suspenseExchange to your urql exchanges
    cacheExchange,
    fetchExchange,
  ],
});

📦 Ecosystem

urql has an extended ecosystem of additional packages that either are "Exchanges" which extend urql's core functionality or are built to make certain tasks easier.

You can find the full list of exchanges in the docs.

💡 Examples

There are currently three examples included in this repository:

Maintenance Status

Active: Formidable is actively working on this project, and we expect to continue for work for the foreseeable future. Bug reports, feature requests and pull requests are welcome.