Настройка 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, кодогенерация типов, хуки для queries/mutations/subscriptions, обработка ошибок, 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 дня.