Skip to content

withGQFnClient

You can set up your favorite client with @gqfn/nuxt.

Setup Schema

ts
const endpoint = 'https://your-graphql-endpoint'
const schema = useGQFnSchema(endpoint)

Setup Client

We provide a simple client by default. But most cases you need to configure your client for advanced features.

Build-in Client

The build-in client use $fetch to send request.

ts
const endpoint = 'https://your-graphql-endpoint'

const schema = useGQFnSchema(endpoint)
const {
  defineOperation,
  defineAsyncQuery,
  defineLazyAsyncQuery,
  defineSubscription,
} = withGQFnClient(schema, {
  fetchOptions: () => ({
    headers: {
      Authorization: `Bearer ${getToken()}`,
    },
  })
})

// define your operations

graphql-request

graphql-request is a minimal GraphQL client supporting Node and browsers for scripts or simple apps.

bash
# ✨ Auto-detect
npx nypm install graphql-request graphql
bash
npm install graphql-request graphql
bash
yarn add graphql-request graphql
bash
pnpm install graphql-request graphql
bash
bun install graphql-request graphql

Then setup your client.

ts
import { GraphQLClient } from 'graphql-request'

const endpoint = 'https://your-graphql-endpoint'

const schema = useGQFnSchema(endpoint)
const client = new GraphQLClient(endpoint, {
  // we suggest to use build-in fetch if have a internal graphql server
  // so that you can save network cost
  fetch: (input, init) => $fetch.raw(
    input.toString(),
    {
      ...init as any,
      responseType: 'stream',
    },
  ),
  // custom options
})

const {
  defineOperation,
  defineAsyncQuery,
  defineLazyAsyncQuery,
  defineSubscription,
} = withGQFnClient(
  schema,
  ({ url, document, variables }, _context) => {
    if (type === 'subscription') {
      return // use other library for subscription
    }

    // @ts-expect-error ignore var not match error
    return client.request({ document, variables })
  },
)

urql

urql is a highly customisable and versatile GraphQL client.

bash
# ✨ Auto-detect
npx nypm install @urql/core
bash
npm install @urql/core
bash
yarn add @urql/core
bash
pnpm install @urql/core
bash
bun install @urql/core

Then setup your client.

ts
import { Client, cacheExchange, fetchExchange } from '@urql/core'
import type { OperationContext } from '@urql/core'

const endpoint = 'https://your-graphql-endpoint'

const client = new Client({
  url: endpoint,
  exchanges: [cacheExchange, fetchExchange],
  // we suggest to use build-in fetch if have a internal graphql server
  // so that you can save network cost
  fetch: (input, init) => $fetch.raw(
    input.toString(),
    {
      ...init as any,
      responseType: 'stream',
    },
  ),
})

const schema = useGQFnSchema(endpoint)
const {
  defineOperation,
  defineAsyncQuery,
  defineLazyAsyncQuery,
} = withGQFnClient<Partial<OperationContext>>(
  schema,
  ({ document, variables, type }, context) => {
    if (type === 'query') {
      return client.query(document, variables, context)
        .toPromise()
    }
    // type === 'mutation'
    return client.mutation(document, variables, context)
      .toPromise()
  },
)

Other Clients

You can use any other GraphQL client with @gqfn/nuxt.

Setup Subscription Client

By default we provide two kinds of subscription client: websocket by graphql-ws and a simple event-source client for sse. For the vast majority of cases, they are enough. But if you need to customize them, you can use the subscription option in withGQFnClient

Build-in Subscription Client

event-source is the default subscription client. You can switch to graphql-ws by setting handler: 'ws'.

ts
const schema = useGQFnSchema(endpoint)
const {
  defineSubscription,
} = withGQFnClient(schema, {
  subscription: { handler: 'ws' }
})

event-source

The only option of event-source is withCredentials.

graphql-ws

Configuration Options of graphql-ws

Other Subscription Clients

See Type Definitions to implement your own subscription client.

Define Operation

withGQFnClient provides some methods for you to define operations composables.

defineOperation

ts
export const useUser = defineOperation(
  gqfn => gqfn('query QueryUser', {
    id: 'ID',
  }, [{
    user: $ => $({ id: $.id }, ['id', 'name']),
  }]),
)

const { user } = await useUser({ id: '1' })

defineAsyncQuery

Create a composable around useAsyncData and defineOperation. It automatically generates a key base on the query and first time variables.

TIP

It only works on query and will throw an error if received a mutation or subscription operation.

ts
export const useAsyncUser = defineAsyncQuery(
  gqfn => gqfn('query QueryUser', {
    id: 'ID',
  }, [{
    user: $ => $({ id: $.id }, ['id', 'name']),
  }]),
)

const { data, pending, error, refresh } = await useAsyncUser({ id: '1' })

You can set the watch option to refresh when the variables changed. You need to provide ref, computed or a getter (() => T) as the variables, so that we can always use the latest value to refresh.

ts
const id = ref('1')
const { data, pending, error, refresh } = await useAsyncUser(
  () => { id: id.value },
  { watch: [id] },
)

Nuxt: useAsyncData

defineLazyAsyncQuery

Same to defineAsyncQuery but with lazy:true by default.

ts
export const useLazyAsyncUser = defineLazyAsyncQuery(
  gqfn => gqfn('query QueryUser', {
    id: 'ID',
  }, [{
    user: $ => $({ id: $.id }, ['id', 'name']),
  }]),
)

const { data, pending, error, refresh } = await useLazyAsyncUser({ id: '1' })

Nuxt: useLazyAsyncData

defineSubscription

Create a composable for your subscription. This composable only works in client-side and will do nothing and throw a warning if used in server-side.

ts
const useCountdown = defineSubscription(
  gqfn => gqfn('subscription', [{
    countdown: $ => $({ from: 3 }, true),
  }]),
)

const num = ref(0)
let cancel: () => void | undefined
async function reset() {
  if (cancel) {
    cancel()
  }
  const countdown = await useCountdown()
  cancel = countdown.unsubscribe
  num.value = countdown.data.value?.countdown ?? 0

  const unwatch = watch(countdown.data, (val) => {
    if (val) {
      num.value = val.countdown
    }
    if (countdown.state.value === 'closed') {
      unwatch()
    }
  })
}

Type Definitions

ts
export function withGQFnClient<
  Context = DefaultHandlerOptions,
  SubscriptionContext = DefaultSubscriptionHandlerOptions,
  Endpoint extends Endpoints = string,
>(
  schema: UseGQFnSchema<Endpoint>,
  options?: WithGQFnClientOptions<Context, SubscriptionContext>,
): WithGQFnClient<Context, Endpoint>
More
ts
export interface WithGQFnClientOptions<
  Context,
  SubscriptionContext,
> {
  handler?: RequestHandler<Context> | DefaultHandlerOptions
  subscription?:
    | ({
      /**
       * Override the default subscription handler.
       * By default it use EventSource
       * - `SubscriptionHandler`: Custom subscription handler.
       * - `ws`: Use websocket handler from package `graphql-ws`.
       * - `sse`: Use default sse handler.
       */
      handler?: 'sse'
    } & SSEOptions)
    | ({
      /**
       * Override the default subscription handler.
       * By default it use EventSource
       * - `SubscriptionHandler`: Custom subscription handler.
       * - `ws`: Use websocket handler from package `graphql-ws`.
       * - `sse`: Use default sse handler.
       */
      handler: 'ws'
    } & WSOptions)
    | {
      /**
       * Override the default subscription handler.
       * By default it use EventSource
       * - `SubscriptionHandler`: Custom subscription handler.
       * - `ws`: Use websocket handler from package `graphql-ws`.
       * - `sse`: Use default sse handler.
       */
      handler: SubscriptionHandler<SubscriptionContext>
    }
}

export type RequestHandler<Context> = <
  TData,
  TVars extends Record<string, unknown>,
> (
  query: {
    document: TypedQueryDocumentNode<TData, TVars>
    variables: NoInfer<TVars>
    type: 'query' | 'mutation'
    url: string
  },
  context?: Context
) => Promise<TData>

export type SubscriptionHandler<Context> = <
  TData,
  TVars extends Record<string, unknown>,
> (
  func: {
    update: (data: TData, isFinal?: boolean) => void
    close: (error?: any) => void
    onUnsubscribe: (fn: () => void) => void
  },
  query: {
    document: TypedQueryDocumentNode<TData, TVars>
    variables: NoInfer<TVars>
    type: 'subscription'
    url: string
  },
  context?: Context
) => void

export interface WithGQFnClient<
  Context,
  Endpoint extends Endpoints = string,
> {
  defineOperation: DefineOperation<Context, Endpoint>
  defineAsyncQuery: DefineAsyncQuery<Context, Endpoint>
  defineLazyAsyncQuery: DefineAsyncQuery<Context, Endpoint>
  defineSubscription: DefineSubscription<Context, Endpoint>
}

export interface DefineOperation<
  Context,
  Endpoint extends Endpoints = string,
> {
  <TData, TVars extends Record<string, unknown>>(
    def: (
      | ((
        gqfn: UseGQFnSchema<Endpoint>['gqfn'],
        $enum: UseGQFnSchema<Endpoint>['$enum'],
      ) => TypedQueryDocumentNode<TData, TVars>)
      | TypedQueryDocumentNode<TData, TVars>
    ),
    context?: Context,
  ): DefineOperationReturn<Promise<TData>, TVars, Context>
}

export interface DefineAsyncQuery<
  Context,
  Endpoint extends Endpoints = string,
> {
  <
    TData,
    TVars extends Record<string, unknown>,
    DataT = TData | undefined,
    PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
    DefaultT = null,
  > (
    def: (
      | ((
        gqfn: UseGQFnSchema<Endpoint>['gqfn'],
        $enum: UseGQFnSchema<Endpoint>['$enum'],
      ) => TypedQueryDocumentNode<TData, TVars>)
      | TypedQueryDocumentNode<TData, TVars>
    ),
    context?: Context,
  ): DefineAsyncQueryReturn<
    AsyncData<PickFrom<DataT, PickKeys> | DefaultT, Error | null>,
    TVars,
    AsyncDataOptions<TData | undefined, DataT, PickKeys, DefaultT> & { context?: Context }
  >

  <
    TData,
    TVars extends Record<string, unknown>,
    DataT = TData | undefined,
    PickKeys extends KeysOf<DataT> = KeysOf<DataT>,
    DefaultT = DataT,
  > (
    def: (
      | ((
        gqfn: UseGQFnSchema<Endpoint>['gqfn'],
        $enum: UseGQFnSchema<Endpoint>['$enum'],
      ) => TypedQueryDocumentNode<TData, TVars>)
      | TypedQueryDocumentNode<TData, TVars>
    ),
    context?: Context,
  ): DefineAsyncQueryReturn<
    AsyncData<PickFrom<DataT, PickKeys> | DefaultT, Error | null>,
    TVars,
    AsyncDataOptions<TData | undefined, DataT, PickKeys, DefaultT> & { context?: Context }
  >
}

export interface DefineSubscription<
  Context,
  Endpoint extends Endpoints = string,
> {
  <TData, TVars extends Record<string, unknown>>(
    def: (
      | ((
        gqfn: UseGQFnSchema<Endpoint>['gqfn'],
        $enum: UseGQFnSchema<Endpoint>['$enum'],
      ) => TypedQueryDocumentNode<TData, TVars>)
      | TypedQueryDocumentNode<TData, TVars>
    ),
    context?: Context,
  ): DefineSubscriptionReturn<TData, TVars, Context>
}

export type DefineOperationReturn<Ret, TVars, Context> =
  Record<string, never> extends TVars
    ? (variables?: TVars, context?: Context) => Ret
    : (variables: TVars, context?: Context) => Ret

export type DefineAsyncQueryReturn<Ret, TVars, Options> =
  Record<string, never> extends TVars
    ? DefineAsyncQueryReturnFnE<Ret, TVars, Options>
    : DefineAsyncQueryReturnFn<Ret, TVars, Options>

export interface DefineAsyncQueryReturnFn<Ret, TVars, Options> {
  (
    variables: TVars,
    options?: Omit<Options, 'watch'> // Force to use getter if watched.
  ): Ret
  (
    variables: Ref<TVars> | (() => TVars),
    options?: Options
  ): Ret
}
export interface DefineAsyncQueryReturnFnE<Ret, TVars, Options> {
  (
    variables?: TVars,
    options?: Omit<Options, 'watch'> // Force to use getter if watched.
  ): Ret
  (
    variables?: Ref<TVars> | (() => TVars),
    options?: Options
  ): Ret
}

export type DefineSubscriptionReturn<Ret, TVars, Context> =
  Record<string, never> extends TVars
    ? (variables?: TVars, options?: Context) => Promise<SubscriptionReturn<Ret>>
    : (variables: TVars, options?: Context) => Promise<SubscriptionReturn<Ret>>

export interface SubscriptionReturn<TData> {
  state: ComputedRef<'pending' | 'connected' | 'closed'>
  data: ComputedRef<TData | undefined>
  error: ComputedRef<Error | null>
  unsubscribe: () => void
  /**
   * Close the current subscription and reconnect.
   */
  restart: () => Promise<void>
  /**
   * Keep the current subscription alive and seamless switch to a new one.
   * This is useful when you have a connection time limit.
   */
  refresh: () => Promise<void>
}