Налаштування URQL для GraphQL у веб-застосунку

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Налаштування URQL для GraphQL у веб-застосунку
Середня
від 1 робочого дня до 3 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Налаштування URQL для GraphQL у веб-додатках

URQL — GraphQL-клієнт, який робить ставку на модульність та простоту. Замість монолітної архітектури Apollo — система exchanges: ланцюг обробників, через який проходить кожен запит. Потрібен кеш? Підключи exchange. Потрібні підписки? Ще один exchange. Не потрібна частина функціональності — не підключай.

Результат: менший bundle size при порівнянних можливостях. З коробки підтримує React, Vue, Svelte та Preact.

Що входить у роботу

Конфігурація клієнта та exchanges, налаштування cacheExchange, authExchange, subscriptionExchange, кодогенерація типів, хуки для запитів/мутацій/підписок, обробка помилок, SSR.

Встановлення

npm install urql graphql
# exchanges
npm install @urql/exchange-auth @urql/exchange-retry
# для підписок
npm install graphql-ws

Базова конфігурація

// lib/urql/client.ts
import {
  createClient,
  cacheExchange,
  fetchExchange,
  subscriptionExchange,
  mapExchange,
} from 'urql'
import { authExchange } from '@urql/exchange-auth'
import { retryExchange } from '@urql/exchange-retry'
import { createClient as createWsClient } from 'graphql-ws'

const wsClient = createWsClient({
  url: import.meta.env.VITE_WS_URL ?? 'ws://localhost:4000/graphql',
  connectionParams: () => ({
    authorization: `Bearer ${localStorage.getItem('token')}`,
  }),
})

export const urqlClient = createClient({
  url: import.meta.env.VITE_GRAPHQL_URL ?? '/graphql',
  exchanges: [
    // порядок важен — зверху вниз
    mapExchange({
      onError(error) {
        if (error.response?.status === 401) {
          authStore.logout()
        }
        console.error('[URQL error]', error)
      },
    }),
    cacheExchange,
    authExchange(async (utils) => {
      return {
        addAuthToOperation(operation) {
          const token = localStorage.getItem('token')
          if (!token) return operation
          return utils.appendHeaders(operation, {
            Authorization: `Bearer ${token}`,
          })
        },
        didAuthError(error) {
          return error.graphQLErrors.some(
            (e) => e.extensions?.code === 'UNAUTHENTICATED'
          )
        },
        async refreshAuth() {
          // можна оновити токен тут
          const newToken = await refreshTokenRequest()
          if (newToken) {
            localStorage.setItem('token', newToken)
          } else {
            localStorage.removeItem('token')
            authStore.logout()
          }
        },
        willAuthError() {
          // перевіряємо токен перед запитом
          const token = localStorage.getItem('token')
          return !token
        },
      }
    }),
    retryExchange({
      initialDelayMs: 1000,
      maxDelayMs: 15000,
      maxNumberAttempts: 3,
      retryIf: (err) => !!(err && err.networkError),
    }),
    fetchExchange,
    subscriptionExchange({
      forwardSubscription(request) {
        const input = { ...request, query: request.query ?? '' }
        return {
          subscribe(sink) {
            const dispose = wsClient.subscribe(input, sink)
            return { unsubscribe: dispose }
          },
        }
      },
    }),
  ],
})
// main.tsx
import { Provider as UrqlProvider } from 'urql'
import { urqlClient } from '@/lib/urql/client'

function App() {
  return (
    <UrqlProvider value={urqlClient}>
      <Router />
    </UrqlProvider>
  )
}

Нормалізований кеш (Graphcache)

Базовий cacheExchange — документний кеш (за ключем = рядок запиту + змінні). Для нормалізованого кешу, як у Apollo:

npm install @urql/exchange-graphcache
import { offlineExchange } from '@urql/exchange-graphcache'
import schema from './schema.json' // introspection schema

const cache = offlineExchange({
  schema,
  keys: {
    Product: (data) => data.id ?? null,
    User: (data) => data.id ?? null,
    Category: (data) => data.id ?? null,
  },
  resolvers: {
    Query: {
      product: (_, args) => ({ __typename: 'Product', id: args.id }),
    },
  },
  updates: {
    Mutation: {
      createProduct: (result, _args, cache) => {
        // оновлюємо всі queries, які містять список products
        cache.invalidate('Query', 'products')
      },
      deleteProduct: (result, args, cache) => {
        cache.invalidate({ __typename: 'Product', id: args.id as string })
      },
      updateProduct: (_result, _args, cache) => {
        // нормалізований кеш оновиться автоматично за id
        // ручна інвалідація не потрібна, якщо __typename + id повертаються
      },
    },
  },
  optimistic: {
    updateProduct: (args) => ({
      __typename: 'Product',
      id: args.id,
      ...(args.input as object),
    }),
  },
})

Кодогенерація

URQL використовує той же @graphql-codegen/client-preset:

// codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli'

const config: CodegenConfig = {
  schema: 'http://localhost:4000/graphql',
  documents: 'src/**/*.graphql',
  generates: {
    'src/gql/': {
      preset: 'client',
      config: {
        useTypeImports: true,
      },
    },
    // introspection для Graphcache
    'src/lib/urql/schema.json': {
      plugins: ['introspection'],
    },
  },
}

export default config

GraphQL-документи

# src/features/products/products.graphql

query GetProducts($categoryId: ID!, $page: Int, $pageSize: Int) {
  products(categoryId: $categoryId, page: $page, pageSize: $pageSize) {
    items {
      id
      name
      price
      stock
    }
    total
    hasNextPage
  }
}

mutation CreateProduct($input: CreateProductInput!) {
  createProduct(input: $input) {
    id
    name
    price
  }
}

subscription OnOrderStatus($orderId: ID!) {
  orderStatusChanged(orderId: $orderId) {
    orderId
    status
    updatedAt
  }
}

Хуки

// features/products/useProducts.ts
import { useQuery, useMutation, useSubscription } from 'urql'
import {
  GetProductsDocument,
  CreateProductDocument,
  OnOrderStatusDocument,
} from '@/gql/graphql'

export function useProducts(categoryId: string, page = 1) {
  const [result, reexecute] = useQuery({
    query: GetProductsDocument,
    variables: { categoryId, page, pageSize: 20 },
    requestPolicy: 'cache-and-network',
  })

  return {
    products: result.data?.products.items ?? [],
    total: result.data?.products.total ?? 0,
    hasNextPage: result.data?.products.hasNextPage ?? false,
    fetching: result.fetching,
    error: result.error,
    refresh: () => reexecute({ requestPolicy: 'network-only' }),
  }
}

export function useCreateProduct() {
  const [result, createProduct] = useMutation(CreateProductDocument)

  return {
    createProduct: (input: CreateProductInput) => createProduct({ input }),
    fetching: result.fetching,
    error: result.error,
  }
}

export function useOrderStatus(orderId: string) {
  const [result] = useSubscription({
    query: OnOrderStatusDocument,
    variables: { orderId },
    pause: !orderId, // призупинити, якщо orderId порожній
  })

  return {
    status: result.data?.orderStatusChanged?.status,
    fetching: result.fetching,
    error: result.error,
  }
}

Використання в компоненті

function ProductList({ categoryId }: { categoryId: string }) {
  const [page, setPage] = useState(1)
  const { products, total, hasNextPage, fetching, error, refresh } = useProducts(categoryId, page)
  const { createProduct, fetching: creating } = useCreateProduct()

  if (fetching && products.length === 0) return <Skeleton />
  if (error) return <ErrorMessage message={error.message} />

  return (
    <div>
      {fetching && <LoadingBar />}
      <ProductGrid products={products} />
      <Pagination
        page={page}
        total={total}
        hasNext={hasNextPage}
        onNext={() => setPage((p) => p + 1)}
        onPrev={() => setPage((p) => p - 1)}
      />
    </div>
  )
}

requestPolicy

URQL підтримує 4 політики кешу на рівні кожного запиту:

  • cache-first — використовує кеш, не йде в мережу, якщо доступно (за замовчуванням)
  • cache-and-network — повертає кеш та паралельно оновлює
  • network-only — завжди йде в мережу, не записує в кеш
  • cache-only — тільки кеш, без запиту в мережу
const [result] = useQuery({
  query: GetProductDocument,
  variables: { id },
  requestPolicy: 'cache-and-network',
})

Ручна робота з кешем поза компонентом

// інвалідація через клієнт безпосередньо
urqlClient.invalidateQuery(GetProductsDocument, { categoryId: '1' })

// запит через клієнт (поза React)
const result = await urqlClient.query(GetProductDocument, { id: '42' }).toPromise()

SSR з Next.js

// pages/_app.tsx
import { withUrqlClient } from 'next-urql'
import { ssrExchange, cacheExchange, fetchExchange } from 'urql'

export default withUrqlClient(
  (ssrCache) => ({
    url: process.env.GRAPHQL_URL!,
    exchanges: [cacheExchange, ssrCache, fetchExchange],
  }),
  { ssr: true }
)(MyApp)

Що ми робимо

Конфігуруємо ланцюг exchanges під задачі проекту (auth, retry, cache, subscriptions), налаштовуємо кодогенерацію, реалізуємо хуки для всіх операцій, при складних вимогах до кешу — підключаємо Graphcache з нормалізацією та оптимістичними оновленнями.

Строк: 2–4 дні.