# urql GraphQL Client: Lightweight Customizable Client

# urql GraphQL Client  
**Lightweight · Customizable · Framework‑agnostic**  

---

## Table of Contents
1. [What is urql?](#what-is-urql)  
2. [Key Features & Design Goals](#key-features)  
3. [Getting Started – Installation & Basic Setup](#setup)  
4. [The Core Hook (`useQuery`) – A Minimal Example](#hook-example)  
5. [Five Common Patterns]  
   1. [Fetching Data – Queries](#pattern‑query)  
   2. [Changing Data – Mutations](#pattern‑mutation)  
   3. [Real‑time Updates – Subscriptions](#pattern‑subscription)  
   4. [Authentication & Token Refresh](#pattern‑auth)  
   5. [Advanced Caching with `graphcache`](#pattern‑cache)  
6. [Frequently Asked Questions (FAQ)](#faq)  
7. [Conclusion – When to Choose urql?](#conclusion)  

---

<a name="what-is-urql"></a>
## 1. What is urql?

`urql` (pronounced “Ur‑kel”) is an open‑source GraphQL client created by Formidable and now maintained by Nearform. It ships as a **single, tiny npm package** that works out of the box with React, React Native, Preact, Vue, Solid, Svelte and even plain JavaScript.  

Unlike “batteries‑included” clients such as Apollo, urql starts with a **minimal core** (≈ 7 KB gzipped) and lets you **opt‑in** to extra capabilities through a plug‑in system called **exchanges**. An exchange is essentially a middleware that can inspect, modify, or short‑circuit any GraphQL operation (query, mutation, or subscription). This design gives you fine‑grained control over caching, retries, authentication, logging, and more—without paying the cost for features you never use.

---

<a name="key-features"></a>
## 2. Key Features & Design Goals

| Feature | Why it matters |
|---------|----------------|
| **Lightweight core** | Small bundle size keeps page‑load times low, especially on mobile. |
| **Composable exchanges** | Add or replace behavior (e.g., `cacheExchange`, `authExchange`, `retryExchange`) in a predictable order. |
| **Document caching by default** | Every query result is cached by its GraphQL document + variables, giving instant “stale‑while‑revalidate” behavior. |
| **Normalized `graphcache`** | Optional normalized cache for optimistic updates, pagination, and offline support. |
| **First‑class hooks** | `useQuery`, `useMutation`, `useSubscription` integrate naturally with React’s rendering model. |
| **Framework‑agnostic bindings** | Same client can be used in React, Vue, Svelte, Solid, etc., via thin adapters. |
| **DevTools extension** | Real‑time inspection of operations, cache state, and exchange flow. |
| **SSR/SSG ready** | `ssrExchange` enables server‑side rendering and static‑site generation with zero extra code. |

---

<a name="setup"></a>
## 3. Getting Started – Installation & Basic Setup

### 3.1 Install the core package (and optional extras)

```bash
# Core client – works for any framework
npm i urql

# Optional: normalized cache (graphcache) and auth exchange
npm i @urql/exchange-graphcache @urql/exchange-auth
```

> **Tip:** If you are only doing simple queries, you can stop after installing `urql`. The extra packages are lazy‑loaded only when you add them to the exchange chain.

### 3.2 Create a client

```tsx
// src/urqlClient.ts
import { createClient, dedupExchange, cacheExchange, fetchExchange } from 'urql';

// Optional: import extra exchanges
import { authExchange } from '@urql/exchange-auth';
import { cacheExchange as graphCache } from '@urql/exchange-graphcache';

export const client = createClient({
  url: 'https://my-api.example.com/graphql',
  // The order of exchanges matters – they are executed left‑to‑right.
  exchanges: [
    dedupExchange,               // De‑duplicate identical requests
    cacheExchange,               // Simple document cache (default)
    // graphCache({ /* config */ }), // Uncomment for normalized cache
    // authExchange({ /* config */ }), // Uncomment for auth handling
    fetchExchange,               // Performs the actual HTTP request
  ],
});
```

### 3.3 Provide the client to your UI tree

```tsx
// src/App.tsx
import React from 'react';
import { Provider } from 'urql';
import { client } from './urqlClient';
import Home from './Home';

function App() {
  return (
    <Provider value={client}>
      <Home />
    </Provider>
  );
}

export default App;
```

That’s all you need to have a fully functional GraphQL client. From here you can start using the hooks or the render‑prop components that ship with the binding of your choice.

---

<a name="hook-example"></a>
## 4. The Core Hook (`useQuery`) – A Minimal Example

```tsx
// src/components/Hello.tsx
import React from 'react';
import { useQuery } from 'urql';
import { gql } from 'graphql-tag';

const HELLO_QUERY = gql`
  query {
    hello
  }
`;

export default function Hello() {
  const [{ data, fetching, error }] = useQuery({ query: HELLO_QUERY });

  if (fetching) return <p>Loading…</p>;
  if (error) return <p>❌ {error.message}</p>;

  return <p>👋 {data?.hello}</p>;
}
```

**What happens under the hood?**

1. `useQuery` registers the operation with the client.  
2. The client checks the **document cache** – if a fresh result exists, it returns it immediately.  
3. If not, the request flows through the exchange chain, eventually reaching `fetchExchange` which performs a `POST` to the GraphQL endpoint.  
4. When the response arrives, the cache is updated and the component re‑renders with the new data.

Because the hook returns an array (`[result, reexecuteQuery]`), you can also **re‑execute** the query manually (e.g., after a mutation) – a pattern we’ll revisit later.

---

<a name="pattern-query"></a>
## 5. Five Common Patterns

Below are five practical patterns that cover the majority of real‑world use‑cases. Each pattern shows the minimal code required and explains which exchanges you might want to add.

### 5.1 Pattern 1 – Fetching Data (Queries)

**Goal:** Retrieve data, handle loading/error states, and optionally refetch on demand.

```tsx
import { useQuery } from 'urql';
import { gql } from 'graphql-tag';

const POSTS_QUERY = gql`
  query Posts($limit: Int!) {
    posts(limit: $limit) {
      id
      title
      author {
        name
      }
    }
  }
`;

export function Posts({ limit = 10 }) {
  const [{ data, fetching, error }, reexecuteQuery] = useQuery({
    query: POSTS_QUERY,
    variables: { limit },
  });

  if (fetching) return <p>Loading posts…</p>;
  if (error) return <p>Failed: {error.message}</p>;

  return (
    <>
      <ul>
        {data.posts.map(p => (
          <li key={p.id}>
            <strong>{p.title}</strong> – {p.author.name}
          </li>
        ))}
      </ul>
      <button onClick={() => reexecuteQuery({ requestPolicy: 'network-only' })}>
        Refresh
      </button>
    </>
  );
}
```

**Why it works:**  
- The default `cacheExchange` returns cached results instantly, then silently updates if the network response differs.  
- `requestPolicy: 'network-only'` forces a fresh fetch, useful for “pull‑to‑refresh” UI.

### 5.2 Pattern 2 – Changing Data (Mutations)

**Goal:** Send a mutation, optimistically update the UI, and keep the cache in sync.

```tsx
import { useMutation } from 'urql';
import { gql } from 'graphql-tag';

const ADD_TODO = gql`
  mutation AddTodo($text: String!) {
    addTodo(text: $text) {
      id
      text
      completed
    }
  }
`;

export function AddTodo() {
  const [, addTodo] = useMutation(ADD_TODO);
  const [text, setText] = React.useState('');

  const handleSubmit = async e => {
    e.preventDefault();
    await addTodo(
      { text },
      {
        // Optimistic UI – show the new todo instantly
        optimistic: {
          addTodo: {
            __typename: 'Todo',
            id: Math.random().toString(),
            text,
            completed: false,
          },
        },
      }
    );
    setText('');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={text} onChange={e => setText(e.target.value)} placeholder="New todo" />
      <button type="submit">Add</button>
    </form>
  );
}
```

**Key points**

- `useMutation` returns a tuple `[result, executeMutation]`.  
- The second argument to `executeMutation` can contain an `optimistic` field that urql merges into the cache immediately.  
- If you have **graphcache** enabled, you can also write an `updates` config to automatically insert the new todo into the `todos` query result.

### 5.3 Pattern 3 – Real‑time Updates (Subscriptions)

**Goal:** Subscribe to a GraphQL subscription and render live data (e.g., chat messages).

> **Prerequisite:** Install a WebSocket transport (`subscriptions-transport-ws` or `graphql-ws`) and add the `subscriptionExchange`.

```bash
npm i graphql-ws
```

```tsx
// src/urqlClient.ts (add subscription exchange)
import { createClient, dedupExchange, cacheExchange, fetchExchange } from 'urql';
import { subscriptionExchange } from '@urql/exchange-graphcache';
import { createClient as createWSClient } from 'graphql-ws';

const wsClient = createWSClient({
  url: 'wss://my-api.example.com/graphql',
});

export const client = createClient({
  url: 'https://my-api.example.com/graphql',
  exchanges: [
    dedupExchange,
    cacheExchange,
    subscriptionExchange({
      forwardSubscription: operation => ({
        subscribe: sink => ({
          unsubscribe: wsClient.subscribe(operation, sink),
        }),
      }),
    }),
    fetchExchange,
  ],
});
```

```tsx
// src/components/Chat.tsx
import { useSubscription } from 'urql';
import { gql } from 'graphql-tag';

const MESSAGE_SUB = gql`
  subscription {
    messageAdded {
      id
      author
      content
    }
  }
`;

export function Chat() {
  const [{ data, error }] = useSubscription({ query: MESSAGE_SUB });

  if (error) return <p>Subscription error: {error.message}</p>;

  return (
    <ul>
      {data?.messageAdded && (
        <li key={data.messageAdded.id}>
          <strong>{data.messageAdded.author}:</strong> {data.messageAdded.content}
        </li>
      )}
    </ul>
  );
}
```

**Why it works:**  
- The `subscriptionExchange` intercepts operations with `operation.kind === 'subscription'` and forwards them to the WebSocket client.  
- Incoming payloads are merged into the cache, so any component that reads the same query will automatically update.

### 5.4 Pattern 4 – Authentication & Token Refresh

**Goal:** Attach an `Authorization` header to every request, automatically refresh expired tokens, and handle logout on auth errors.

```tsx
// src/urqlClient.ts (add auth exchange)
import { authExchange } from '@urql/exchange-auth';
import { createClient, dedupExchange, cacheExchange, fetchExchange } from 'urql';

function getAuth() {
  const token = localStorage.getItem('access_token');
  return token ? { token } : null;
}

export const client = createClient({
  url: 'https://my-api.example.com/graphql',
  exchanges: [
    dedupExchange,
    cacheExchange,
    authExchange({
      addAuthToOperation: ({ authState, operation }) => {
        if (!authState?.token) return operation;
        const fetchOptions = {
          ...operation.context.fetchOptions,
          headers: {
            ...operation.context.fetchOptions?.headers,
            Authorization: `Bearer ${authState.token}`,
          },
        };
        return { ...operation, context: { ...operation.context, fetchOptions } };
      },
      // Called when the client is first created
      getAuth: async ({ authState }) => {
        if (!authState) {
          // No token yet – try to read from storage
          const token = localStorage.getItem('access_token');
          return token ? { token } : null;
        }
        // Token exists – check expiry and refresh if needed
        const isExpired = /* your logic */;
        if (!isExpired) return null; // No refresh needed

        // Example refresh flow
        const response = await fetch('https://my-api.example.com/refresh', {
          method: 'POST',
          credentials: 'include',
        });
        const { access_token } = await response.json();
        localStorage.setItem('access_token', access_token);
        return { token: access_token };
      },
      // Optional: handle auth errors globally
      willAuthError: ({ authState }) => {
        // Return true if token is missing or clearly expired
        return !authState?.token;
      },
    }),
    fetchExchange,
  ],
});
```

**How it works**

- `authExchange` runs **before** `fetchExchange`.  
- `addAuthToOperation` injects the `Authorization` header.  
- `getAuth` is invoked when the client starts and whenever `willAuthError` returns `true`. It can perform a token refresh and return a new auth state.  
- If a request fails with a GraphQL error that signals “FORBIDDEN”, you can also add a `mapExchange` to intercept and trigger a logout.

### 5.5 Pattern 5 – Advanced Normalized Caching (`graphcache`)

**Goal:** Enable pagination, optimistic updates, and automatic cache invalidation for complex UI.

```tsx
// src/urqlClient.ts (add graphcache)
import { cacheExchange } from '@urql/exchange-graphcache';

export const client = createClient({
  url: 'https://my-api.example.com/graphql',
  exchanges: [
    dedupExchange,
    cacheExchange({
      keys: {
        // Tell graphcache how to uniquely identify custom types
        Todo: data => data.id,
      },
      resolvers: {
        // Cursor‑based pagination for the `posts` field
        Query: {
          posts: cursorPagination(),
        },
      },
      updates: {
        Mutation: {
          // After adding a todo, insert it into the cached `todos` query
          addTodo: (result, args, cache, info) => {
            const allTodos = cache.resolve('Query', 'todos') as string[];
            cache.link('Query', 'todos', [...allTodos, result.addTodo.__ref]);
          },
        },
      },
    }),
    fetchExchange,
  ],
});
```

**What you gain**

| Feature | Implementation |
|---------|----------------|
| **Pagination** | `cursorPagination()` (built‑in) or a custom resolver. |
| **Optimistic updates** | Provide `optimistic` objects in `useMutation` – graphcache merges them into the normalized store. |
| **Cache invalidation** | Use `cache.invalidate` inside mutation `updates` to force a refetch of stale queries. |
| **Entity relationships** | Define `keys` so the cache can deduplicate objects across queries. |

With graphcache you can treat the client as a **local data store**, dramatically reducing the number of network round‑trips for UI‑heavy applications.

---

<a name="faq"></a>
## 6. Frequently Asked Questions (FAQ)

| Question | Answer |
|----------|--------|
| **Is urql only for React?** | No. The core client (`@urql/core`) works with any JavaScript environment. Framework‑specific bindings (`@urql/preact`, `@urql/vue`, `@urql/svelte`, `@urql/solid`) are thin adapters that expose the same hooks/components. |
| **How does urql differ from Apollo Client?** | Apollo ships with a large feature set (state management, type policies, devtools, etc.) that you pay for even if you never use them. urql starts minimal and lets you **compose** only the pieces you need via exchanges. This results in a smaller bundle and clearer mental model. |
| **Can I use urql with Next.js SSR?** | Absolutely. Add the `ssrExchange` to the exchange list, create a client per request on the server, and hydrate it on the client. The docs contain a full Next.js example. |
| **Do I need TypeScript?** | No, but urql ships with full TypeScript definitions, and the community strongly recommends using them for better autocomplete and safety. |
| **What is the performance impact of the default document cache?** | The cache is an in‑memory `Map` keyed by the query document + serialized variables. Lookups are O(1) and the cache never grows beyond the number of distinct queries you run, making it negligible for most apps. |
| **How do I debug exchange flow?** | Install the **urql DevTools** Chrome/Firefox extension. It visualizes each operation as it passes through exchanges, shows cache snapshots, and lets you replay requests. |
| **Is there a built‑in retry mechanism?** | Yes. Add the `retryExchange` (from `@urql/exchange-retry`) to the chain. You can configure exponential back‑off, max attempts, and which error codes trigger a retry. |
| **Can I use urql with server‑side GraphQL (e.g., Hasura, Prisma)?** | urql is transport‑agnostic; it works with any GraphQL‑compliant endpoint, including Hasura, Prisma, AWS AppSync, and custom servers. |
| **What about file uploads?** | Use the `multipartFetchExchange` from `@urql/exchange-multipart-fetch`. It detects `File` objects in variables and sends a multipart/form‑data request. |
| **Is there a way to batch multiple queries into a single HTTP request?** | Yes. Add the `dedupExchange` (already included) for de‑duplication, and the `batchExchange` (`@urql/exchange-batch`) to combine multiple operations that occur within the same tick. |

---

<a name="conclusion"></a>
## 7. Conclusion – When to Choose urql?

`urql` shines in scenarios where **bundle size, flexibility, and a clear mental model** are priorities:

* **Small‑to‑medium apps** that need a quick GraphQL hookup without the overhead of Apollo’s cache policies.  
* **Performance‑critical front‑ends** (mobile web, PWAs) where every kilobyte matters.  
* **Projects that evolve** – you can start with the bare client and later add `graphcache`, `authExchange`, `retryExchange`, or custom middleware as the app grows.  
* **Multi‑framework codebases** – the same client instance can be shared between a React admin panel and a Svelte widget, reducing duplication.  

If you need an all‑in‑one solution with built‑in state management, optimistic UI, and a massive ecosystem, Apollo may still be the right choice. But for developers who value **explicitness, composability, and a lean footprint**, urql offers a modern, battle‑tested alternative that scales from a single query to a full‑blown offline‑first application.

--- 

*Happy querying!*
