Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.

(vue) - Implement client handles with methods to call hooks (#1599)

+5
.changeset/beige-candles-kneel.md
···
+
---
+
'@urql/vue': minor
+
---
+
+
A `useClientHandle()` function has been added. This creates a `handle` on which all `use*` hooks can be called, like `await handle.useQuery(...)` or `await handle.useSubscription(...)` which is useful for sequentially chaining hook calls in an `async setup()` function or preserve the right instance of a `Client` across lifecycle hooks.
+5
.changeset/fluffy-ligers-draw.md
···
+
---
+
'@urql/vue': patch
+
---
+
+
The `useClient()` function will now throw a more helpful error when it's called outside of any lifecycle hooks.
+16 -2
docs/api/vue.md
···
[Read more about how to use the `useSubscription` API on the "Subscriptions"
page.](../advanced/subscriptions.md#vue)
+
## useClientHandle
+
+
The `useClientHandle()` function may, like the other `use*` functions, be called either in
+
`setup()` or another lifecycle hook, and returns a so called "client handle". Using this `handle` we
+
can access the [`Client`](./core.md#client) directly via the `client` property or call the other
+
`use*` functions as methods, which will be directly bound to this `client`. This may be useful when
+
chaining these methods inside an `async setup()` lifecycle function.
+
+
| Method | Description |
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------- |
+
| `client` | Contains the raw [`Client`](./core.md#client) reference, which allows the `Client` to be used directly. |
+
| `useQuery(...)` | Accepts the same arguments as the `useQuery` function, but will always use the `Client` from the handle's context. |
+
| `useMutation(...)` | Accepts the same arguments as the `useMutation` function, but will always use the `Client` from the handle's context. |
+
| `useSubscription(...)` | Accepts the same arguments as the `useSubscription` function, but will always use the `Client` from the handle's context. |
+
## Context API
In Vue the [`Client`](./core.md#client) is provided either to your app or to a parent component of a
given subtree and is then subsequently injected whenever one of the above composition functions is
used.
-
You can manually retrieve the `Client` in your component by calling `useClient`. Symmetrically you
-
can provide the `Client` from any of your components using the `provideClient` function.
+
You can provide the `Client` from any of your components using the `provideClient` function.
Alternatively, `@urql/vue` also has a default export of a [Vue Plugin function](https://v3.vuejs.org/guide/plugins.html#using-a-plugin).
Both `provideClient` and the plugin function either accept an [instance of
+46
docs/basics/vue.md
···
suspends this component will switch to using its `#fallback` template rather than its `#default`
template.
+
### Chaining calls in Vue Suspense
+
+
As shown [above](#vue-suspense), in Vue Suspense the `async setup()` lifecycle function can be used
+
to set up queries in advance, wait for them to have fetched some data, and then let the component
+
render as usual.
+
+
However, because the `async setup()` function can be used with `await`-ed promise calls, we may run
+
into situations where we're trying to call functions like `useQuery()` after we've already awaited
+
another promise and will be outside of the synchronous scope of the `setup()` lifecycle. This means
+
that the `useQuery` (and `useSubscription` & `useMutation`) functions won't have access to the
+
`Client` anymore that we'd have set up using `provideClient`.
+
+
To prevent this, we can create something called a "client handle" using the `useClientHandle`
+
function.
+
+
```js
+
import { gql, useClientHandle } from '@urql/vue';
+
+
export default {
+
async setup() {
+
const handle = useClientHandle();
+
+
await Promise.resolve(); // NOTE: This could be any await call
+
+
const result = await handle.useQuery({
+
query: gql`
+
{
+
todos {
+
id
+
title
+
}
+
}
+
`,
+
});
+
+
return { data: result.data };
+
},
+
};
+
```
+
+
As we can see, when we use `handle.useQuery()` we're able to still create query results although we've
+
interrupted the synchronous `setup()` lifecycle with a `Promise.resolve()` delay. This would also
+
allow us to create chained queries by using
+
[`computed`](https://v3.vuejs.org/guide/reactivity-computed-watchers.html#computed-values) to use an
+
output from a preceding result in a next `handle.useQuery()` call.
+
### Reading on
There are some more tricks we can use with `useQuery`. [Read more about its API in the API docs for
+1 -1
packages/vue-urql/example/package.json
···
},
"dependencies": {
"@urql/vue": "link:../",
-
"vue": "^3.0.2"
+
"vue": "^3.0.11"
},
"devDependencies": {
"@vue/compiler-sfc": "^3.0.2",
+1 -1
packages/vue-urql/example/src/App.vue
···
name: 'App',
setup() {
provideClient({
-
url: 'https://countries-274616.ew.r.appspot.com/',
+
url: 'https://trygql.dev/graphql/basic-pokedex',
});
},
components: {
+44 -11
packages/vue-urql/example/src/components/HelloWorld.vue
···
<template>
<div>
-
<div v-if="data">
-
<pre>{{ JSON.stringify(data) }}</pre>
+
<div v-if="pokemons">
+
<pre>{{ JSON.stringify(pokemons) }}</pre>
+
</div>
+
<div v-if="pokemon">
+
<pre>{{ JSON.stringify(pokemon) }}</pre>
</div>
+
<button @click="nextPokemon">Next Pokemon</button>
</div>
</template>
<script>
-
import { useQuery } from '@urql/vue';
+
import { ref, computed } from 'vue';
+
import { gql, useClientHandle } from '@urql/vue';
export default {
async setup() {
-
const query = `
-
query {
-
Country {
-
name
+
const handle = useClientHandle();
+
+
const pokemons = await handle.useQuery({
+
query: gql`
+
{
+
pokemons(limit: 10) {
+
id
+
name
+
}
+
}
+
`
+
});
+
+
const index = ref(0);
+
+
const pokemon = await handle.useQuery({
+
query: gql`
+
query ($id: ID!) {
+
pokemon(id: $id) {
+
id
+
name
+
}
}
-
}
-
`;
+
`,
+
variables: {
+
id: computed(() => pokemons.data.value.pokemons[index.value].id),
+
},
+
});
-
const result = useQuery({ query });
-
return { data: result.data };
+
return {
+
pokemons: pokemons.data,
+
pokemon: pokemon.data,
+
nextPokemon() {
+
index.value = index.value < (pokemons.data.value.pokemons.length - 1)
+
? index.value + 1
+
: 0;
+
},
+
};
},
name: 'HelloWorld',
}
+67 -4
packages/vue-urql/example/yarn.lock
···
lodash "^4.17.19"
to-fast-properties "^2.0.0"
+
"@graphql-typed-document-node/core@^3.1.0":
+
version "3.1.0"
+
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.0.tgz#0eee6373e11418bfe0b5638f654df7a4ca6a3950"
+
integrity sha512-wYn6r8zVZyQJ6rQaALBEln5B1pzxb9shV5Ef97kTvn6yVGrqyXVnDqnU24MXnFubR+rZjBY9NWuxX3FB2sTsjg==
+
"@koa/cors@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@koa/cors/-/cors-3.1.0.tgz#618bb073438cfdbd3ebd0e648a76e33b84f3a3b2"
···
"@types/express-serve-static-core" "*"
"@types/mime" "*"
-
"@urql/core@^1.13.1":
-
version "1.13.1"
-
resolved "https://registry.yarnpkg.com/@urql/core/-/core-1.13.1.tgz#7247c27dccd7570010de91730d1f16fd15892829"
-
integrity sha512-Zl4UwvcE9JbWKzrtxnlmfF+rkX50GzK5dpMlB6FnUYF0sLmuGMxp67lnhTQsfTNJ+41bkj4lk0PMWEnG7KUsTw==
+
"@urql/core@^2.0.0":
+
version "2.0.0"
+
resolved "https://registry.yarnpkg.com/@urql/core/-/core-2.0.0.tgz#b4936dd51fe84cb570506d9ee2579149689f7535"
+
integrity sha512-Qj24CG8ullqZZsYmjrSH0JhH+nY7kj8GbVbA9si3KUjlYs75A/MBQU3i97j6oWyGldDBapyis2CfaQeXKbv8rA==
dependencies:
+
"@graphql-typed-document-node/core" "^3.1.0"
wonka "^4.0.14"
"@urql/vue@link:..":
version "0.0.0"
uid ""
+
"@vue/compiler-core@3.0.11":
+
version "3.0.11"
+
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.0.11.tgz#5ef579e46d7b336b8735228758d1c2c505aae69a"
+
integrity sha512-6sFj6TBac1y2cWCvYCA8YzHJEbsVkX7zdRs/3yK/n1ilvRqcn983XvpBbnN3v4mZ1UiQycTvOiajJmOgN9EVgw==
+
dependencies:
+
"@babel/parser" "^7.12.0"
+
"@babel/types" "^7.12.0"
+
"@vue/shared" "3.0.11"
+
estree-walker "^2.0.1"
+
source-map "^0.6.1"
+
"@vue/compiler-core@3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.0.2.tgz#7790b7a1fcbba5ace4d81a70ce59096fa5c95734"
···
"@vue/shared" "3.0.2"
estree-walker "^2.0.1"
source-map "^0.6.1"
+
+
"@vue/compiler-dom@3.0.11":
+
version "3.0.11"
+
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.0.11.tgz#b15fc1c909371fd671746020ba55b5dab4a730ee"
+
integrity sha512-+3xB50uGeY5Fv9eMKVJs2WSRULfgwaTJsy23OIltKgMrynnIj8hTYY2UL97HCoz78aDw1VDXdrBQ4qepWjnQcw==
+
dependencies:
+
"@vue/compiler-core" "3.0.11"
+
"@vue/shared" "3.0.11"
"@vue/compiler-dom@3.0.2", "@vue/compiler-dom@^3.0.2":
version "3.0.2"
···
"@vue/compiler-dom" "3.0.2"
"@vue/shared" "3.0.2"
+
"@vue/reactivity@3.0.11":
+
version "3.0.11"
+
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.0.11.tgz#07b588349fd05626b17f3500cbef7d4bdb4dbd0b"
+
integrity sha512-SKM3YKxtXHBPMf7yufXeBhCZ4XZDKP9/iXeQSC8bBO3ivBuzAi4aZi0bNoeE2IF2iGfP/AHEt1OU4ARj4ao/Xw==
+
dependencies:
+
"@vue/shared" "3.0.11"
+
"@vue/reactivity@3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.0.2.tgz#42ed5af6025b494a5e69b05169fcddf04eebfe77"
integrity sha512-GdRloNcBar4yqWGXOcba1t//j/WizwfthfPUYkjcIPHjYnA/vTEQYp0C9+ZjPdinv1WRK1BSMeN/xj31kQES4A==
dependencies:
"@vue/shared" "3.0.2"
+
+
"@vue/runtime-core@3.0.11":
+
version "3.0.11"
+
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.0.11.tgz#c52dfc6acf3215493623552c1c2919080c562e44"
+
integrity sha512-87XPNwHfz9JkmOlayBeCCfMh9PT2NBnv795DSbi//C/RaAnc/bGZgECjmkD7oXJ526BZbgk9QZBPdFT8KMxkAg==
+
dependencies:
+
"@vue/reactivity" "3.0.11"
+
"@vue/shared" "3.0.11"
"@vue/runtime-core@3.0.2":
version "3.0.2"
···
"@vue/reactivity" "3.0.2"
"@vue/shared" "3.0.2"
+
"@vue/runtime-dom@3.0.11":
+
version "3.0.11"
+
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.0.11.tgz#7a552df21907942721feb6961c418e222a699337"
+
integrity sha512-jm3FVQESY3y2hKZ2wlkcmFDDyqaPyU3p1IdAX92zTNeCH7I8zZ37PtlE1b9NlCtzV53WjB4TZAYh9yDCMIEumA==
+
dependencies:
+
"@vue/runtime-core" "3.0.11"
+
"@vue/shared" "3.0.11"
+
csstype "^2.6.8"
+
"@vue/runtime-dom@3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.0.2.tgz#9d166d03225558025d3d80f5039b646e0051b71c"
···
"@vue/runtime-core" "3.0.2"
"@vue/shared" "3.0.2"
csstype "^2.6.8"
+
+
"@vue/shared@3.0.11":
+
version "3.0.11"
+
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.0.11.tgz#20d22dd0da7d358bb21c17f9bde8628152642c77"
+
integrity sha512-b+zB8A2so8eCE0JsxjL24J7vdGl8rzPQ09hZNhystm+KqSbKcAej1A+Hbva1rCMmTTqA+hFnUSDc5kouEo0JzA==
"@vue/shared@3.0.2":
version "3.0.2"
···
slash "^3.0.0"
vue "^3.0.2"
ws "^7.3.1"
+
+
vue@^3.0.11:
+
version "3.0.11"
+
resolved "https://registry.yarnpkg.com/vue/-/vue-3.0.11.tgz#c82f9594cbf4dcc869241d4c8dd3e08d9a8f4b5f"
+
integrity sha512-3/eUi4InQz8MPzruHYSTQPxtM3LdZ1/S/BvaU021zBnZi0laRUyH6pfuE4wtUeLvI8wmUNwj5wrZFvbHUXL9dw==
+
dependencies:
+
"@vue/compiler-dom" "3.0.11"
+
"@vue/runtime-dom" "3.0.11"
+
"@vue/shared" "3.0.11"
vue@^3.0.2:
version "3.0.2"
+1 -1
packages/vue-urql/package.json
···
},
"devDependencies": {
"graphql": "^15.1.0",
-
"vue": "^3.0.2"
+
"vue": "^3.0.11"
},
"peerDependencies": {
"graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0",
+26 -4
packages/vue-urql/src/index.ts
···
export * from '@urql/core';
-
export * from './useClient';
-
export * from './useQuery';
-
export * from './useMutation';
-
export * from './useSubscription';
+
+
export * from './useClientHandle';
+
export { install, provideClient } from './useClient';
+
+
export {
+
useQuery,
+
UseQueryArgs,
+
UseQueryResponse,
+
UseQueryState,
+
} from './useQuery';
+
+
export {
+
useSubscription,
+
UseSubscriptionArgs,
+
UseSubscriptionResponse,
+
UseSubscriptionState,
+
SubscriptionHandlerArg,
+
SubscriptionHandler,
+
} from './useSubscription';
+
+
export {
+
useMutation,
+
UseMutationResponse,
+
UseMutationState,
+
} from './useMutation';
+
import { install } from './useClient';
export default install;
+7 -1
packages/vue-urql/src/useClient.ts
···
-
import { App, inject, provide } from 'vue';
+
import { App, getCurrentInstance, inject, provide } from 'vue';
import { Client, ClientOptions } from '@urql/core';
export function provideClient(opts: ClientOptions | Client) {
···
}
export function useClient(): Client {
+
if (process.env.NODE_ENV !== 'production' && !getCurrentInstance()) {
+
throw new Error(
+
'use* functions may only be called during the `setup()` or other lifecycle hooks.'
+
);
+
}
+
const client = inject('$urql') as Client;
if (process.env.NODE_ENV !== 'production' && !client) {
throw new Error(
+104
packages/vue-urql/src/useClientHandle.ts
···
+
import { DocumentNode } from 'graphql';
+
import { Client, TypedDocumentNode } from '@urql/core';
+
import {
+
WatchStopHandle,
+
getCurrentInstance,
+
onMounted,
+
onBeforeUnmount,
+
} from 'vue';
+
+
import { useClient } from './useClient';
+
+
import { callUseQuery, UseQueryArgs, UseQueryResponse } from './useQuery';
+
+
import { callUseMutation, UseMutationResponse } from './useMutation';
+
+
import {
+
callUseSubscription,
+
UseSubscriptionArgs,
+
SubscriptionHandlerArg,
+
UseSubscriptionResponse,
+
} from './useSubscription';
+
+
export interface ClientHandle {
+
client: Client;
+
+
useQuery<T = any, V = object>(
+
args: UseQueryArgs<T, V>
+
): UseQueryResponse<T, V>;
+
+
useSubscription<T = any, R = T, V = object>(
+
args: UseSubscriptionArgs<T, V>,
+
handler?: SubscriptionHandlerArg<T, R>
+
): UseSubscriptionResponse<T, R, V>;
+
+
useMutation<T = any, V = any>(
+
query: TypedDocumentNode<T, V> | DocumentNode | string
+
): UseMutationResponse<T, V>;
+
}
+
+
export function useClientHandle(): ClientHandle {
+
const client = useClient();
+
const stops: WatchStopHandle[] = [];
+
+
onBeforeUnmount(() => {
+
let stop: WatchStopHandle | void;
+
while ((stop = stops.shift())) stop();
+
});
+
+
const handle: ClientHandle = {
+
client,
+
+
useQuery<T = any, V = object>(
+
args: UseQueryArgs<T, V>
+
): UseQueryResponse<T, V> {
+
return callUseQuery(args, client, stops);
+
},
+
+
useSubscription<T = any, R = T, V = object>(
+
args: UseSubscriptionArgs<T, V>,
+
handler?: SubscriptionHandlerArg<T, R>
+
): UseSubscriptionResponse<T, R, V> {
+
return callUseSubscription(args, handler, client, stops);
+
},
+
+
useMutation<T = any, V = any>(
+
query: TypedDocumentNode<T, V> | DocumentNode | string
+
): UseMutationResponse<T, V> {
+
return callUseMutation(query, client);
+
},
+
};
+
+
if (process.env.NODE_ENV !== 'production') {
+
onMounted(() => {
+
Object.assign(handle, {
+
useQuery<T = any, V = object>(
+
args: UseQueryArgs<T, V>
+
): UseQueryResponse<T, V> {
+
if (process.env.NODE_ENV !== 'production' && !getCurrentInstance()) {
+
throw new Error(
+
'`handle.useQuery()` should only be called in the `setup()` or a lifecycle hook.'
+
);
+
}
+
+
return callUseQuery(args, client, stops);
+
},
+
+
useSubscription<T = any, R = T, V = object>(
+
args: UseSubscriptionArgs<T, V>,
+
handler?: SubscriptionHandlerArg<T, R>
+
): UseSubscriptionResponse<T, R, V> {
+
if (process.env.NODE_ENV !== 'production' && !getCurrentInstance()) {
+
throw new Error(
+
'`handle.useSubscription()` should only be called in the `setup()` or a lifecycle hook.'
+
);
+
}
+
+
return callUseSubscription(args, handler, client, stops);
+
},
+
});
+
});
+
}
+
+
return handle;
+
}
+5 -8
packages/vue-urql/src/useMutation.test.ts
···
-
jest.mock('vue', () => {
-
const vue = jest.requireActual('vue');
+
jest.mock('./useClient.ts', () => ({
+
__esModule: true,
+
...jest.requireActual('./useClient.ts'),
+
useClient: () => client,
+
}));
-
return {
-
__esModule: true,
-
...vue,
-
inject: () => client,
-
};
-
});
import { makeSubject, pipe, take, toPromise } from 'wonka';
import { createClient } from '@urql/core';
import { useMutation } from './useMutation';
+9 -1
packages/vue-urql/src/useMutation.ts
···
+
/* eslint-disable react-hooks/rules-of-hooks */
+
import { ref, Ref } from 'vue';
import { DocumentNode } from 'graphql';
import {
+
Client,
TypedDocumentNode,
CombinedError,
Operation,
···
export function useMutation<T = any, V = any>(
query: TypedDocumentNode<T, V> | DocumentNode | string
): UseMutationResponse<T, V> {
-
const client = useClient();
+
return callUseMutation(query);
+
}
+
export function callUseMutation<T = any, V = any>(
+
query: TypedDocumentNode<T, V> | DocumentNode | string,
+
client: Client = useClient()
+
): UseMutationResponse<T, V> {
const data: Ref<T | undefined> = ref();
const stale: Ref<boolean> = ref(false);
const fetching: Ref<boolean> = ref(false);
+5 -9
packages/vue-urql/src/useQuery.test.ts
···
-
jest.mock('vue', () => {
-
const vue = jest.requireActual('vue');
-
-
return {
-
__esModule: true,
-
...vue,
-
inject: () => client,
-
};
-
});
+
jest.mock('./useClient.ts', () => ({
+
__esModule: true,
+
...jest.requireActual('./useClient.ts'),
+
useClient: () => client,
+
}));
import { pipe, makeSubject, fromValue, delay } from 'wonka';
import { createClient } from '@urql/core';
+76 -59
packages/vue-urql/src/useQuery.ts
···
-
import { Ref, ref, watchEffect, reactive, isRef } from 'vue';
+
/* eslint-disable react-hooks/rules-of-hooks */
+
import { DocumentNode } from 'graphql';
+
+
import { WatchStopHandle, Ref, ref, watchEffect, reactive, isRef } from 'vue';
import {
Source,
···
} from 'wonka';
import {
+
Client,
OperationResult,
TypedDocumentNode,
CombinedError,
···
}
export function useQuery<T = any, V = object>(
-
_args: UseQueryArgs<T, V>
+
args: UseQueryArgs<T, V>
+
): UseQueryResponse<T, V> {
+
return callUseQuery(args);
+
}
+
+
export function callUseQuery<T = any, V = object>(
+
_args: UseQueryArgs<T, V>,
+
client: Client = useClient(),
+
stops: WatchStopHandle[] = []
): UseQueryResponse<T, V> {
const args = reactive(_args);
-
const client = useClient();
const data: Ref<T | undefined> = ref();
const stale: Ref<boolean> = ref(false);
···
(query$: undefined | Source<OperationResult<T, V>>) => void
> = ref(null as any);
-
watchEffect(() => {
-
const newRequest = createRequest<T, V>(args.query, args.variables as any);
-
if (request.value.key !== newRequest.key) {
-
request.value = newRequest;
-
}
-
}, watchOptions);
+
stops.push(
+
watchEffect(() => {
+
const newRequest = createRequest<T, V>(args.query, args.variables as any);
+
if (request.value.key !== newRequest.key) {
+
request.value = newRequest;
+
}
+
}, watchOptions)
+
);
const state: UseQueryState<T, V> = {
data,
···
const getState = () => state;
-
watchEffect(
-
onInvalidate => {
-
const subject = makeSubject<Source<any>>();
-
source.value = pipe(subject.source, replayOne);
-
next.value = (value: undefined | Source<any>) => {
-
const query$ = pipe(
-
value
-
? pipe(
-
value,
-
onStart(() => {
-
fetching.value = true;
-
stale.value = false;
-
}),
-
onPush(res => {
-
data.value = res.data;
-
stale.value = !!res.stale;
-
fetching.value = false;
-
error.value = res.error;
-
operation.value = res.operation;
-
extensions.value = res.extensions;
-
}),
-
share
-
)
-
: fromValue(undefined),
-
onEnd(() => {
-
fetching.value = false;
-
stale.value = false;
-
})
+
stops.push(
+
watchEffect(
+
onInvalidate => {
+
const subject = makeSubject<Source<any>>();
+
source.value = pipe(subject.source, replayOne);
+
next.value = (value: undefined | Source<any>) => {
+
const query$ = pipe(
+
value
+
? pipe(
+
value,
+
onStart(() => {
+
fetching.value = true;
+
stale.value = false;
+
}),
+
onPush(res => {
+
data.value = res.data;
+
stale.value = !!res.stale;
+
fetching.value = false;
+
error.value = res.error;
+
operation.value = res.operation;
+
extensions.value = res.extensions;
+
}),
+
share
+
)
+
: fromValue(undefined),
+
onEnd(() => {
+
fetching.value = false;
+
stale.value = false;
+
})
+
);
+
+
subject.next(query$);
+
};
+
+
onInvalidate(
+
pipe(source.value, switchAll, map(getState), publish).unsubscribe
);
+
},
+
{
+
// NOTE: This part of the query pipeline is only initialised once and will need
+
// to do so synchronously
+
flush: 'sync',
+
}
+
)
+
);
-
subject.next(query$);
-
};
-
-
onInvalidate(
-
pipe(source.value, switchAll, map(getState), publish).unsubscribe
+
stops.push(
+
watchEffect(() => {
+
next.value(
+
!isPaused.value
+
? client.executeQuery<T, V>(request.value, {
+
requestPolicy: args.requestPolicy,
+
...args.context,
+
})
+
: undefined
);
-
},
-
{
-
// NOTE: This part of the query pipeline is only initialised once and will need
-
// to do so synchronously
-
flush: 'sync',
-
}
+
}, watchOptions)
);
-
-
watchEffect(() => {
-
next.value(
-
!isPaused.value
-
? client.executeQuery<T, V>(request.value, {
-
requestPolicy: args.requestPolicy,
-
...args.context,
-
})
-
: undefined
-
);
-
}, watchOptions);
const response: UseQueryResponse<T, V> = {
...state,
+5 -8
packages/vue-urql/src/useSubscription.test.ts
···
-
jest.mock('vue', () => {
-
const vue = jest.requireActual('vue');
+
jest.mock('./useClient.ts', () => ({
+
__esModule: true,
+
...jest.requireActual('./useClient.ts'),
+
useClient: () => client,
+
}));
-
return {
-
__esModule: true,
-
...vue,
-
inject: () => client,
-
};
-
});
import { makeSubject } from 'wonka';
import { createClient } from '@urql/core';
import { useSubscription } from './useSubscription';
+69 -50
packages/vue-urql/src/useSubscription.ts
···
-
import { Ref, ref, watchEffect, reactive, isRef } from 'vue';
+
/* eslint-disable react-hooks/rules-of-hooks */
+
import { DocumentNode } from 'graphql';
import { Source, pipe, publish, share, onStart, onPush, onEnd } from 'wonka';
+
import { WatchStopHandle, Ref, ref, watchEffect, reactive, isRef } from 'vue';
+
import {
+
Client,
OperationResult,
TypedDocumentNode,
CombinedError,
···
}
export type SubscriptionHandler<T, R> = (prev: R | undefined, data: T) => R;
+
export type SubscriptionHandlerArg<T, R> = MaybeRef<SubscriptionHandler<T, R>>;
export interface UseSubscriptionState<T = any, R = T, V = object> {
fetching: Ref<boolean>;
···
};
export function useSubscription<T = any, R = T, V = object>(
+
args: UseSubscriptionArgs<T, V>,
+
handler?: SubscriptionHandlerArg<T, R>
+
): UseSubscriptionResponse<T, R, V> {
+
return callUseSubscription(args, handler);
+
}
+
+
export function callUseSubscription<T = any, R = T, V = object>(
_args: UseSubscriptionArgs<T, V>,
-
handler?: MaybeRef<SubscriptionHandler<T, R>>
+
handler?: SubscriptionHandlerArg<T, R>,
+
client: Client = useClient(),
+
stops: WatchStopHandle[] = []
): UseSubscriptionResponse<T, R, V> {
const args = reactive(_args);
-
const client = useClient();
const data: Ref<R | undefined> = ref();
const stale: Ref<boolean> = ref(false);
···
const source: Ref<Source<OperationResult<T, V>> | undefined> = ref();
-
watchEffect(() => {
-
const newRequest = createRequest<T, V>(args.query, args.variables as any);
-
if (request.value.key !== newRequest.key) {
-
request.value = newRequest;
-
}
-
}, watchOptions);
+
stops.push(
+
watchEffect(() => {
+
const newRequest = createRequest<T, V>(args.query, args.variables as any);
+
if (request.value.key !== newRequest.key) {
+
request.value = newRequest;
+
}
+
}, watchOptions)
+
);
-
watchEffect(() => {
-
if (!isPaused.value) {
-
source.value = pipe(
-
client.executeSubscription<T, V>(request.value, {
-
...args.context,
-
}),
-
share
-
);
-
} else {
-
source.value = undefined;
-
}
-
}, watchOptions);
+
stops.push(
+
watchEffect(() => {
+
if (!isPaused.value) {
+
source.value = pipe(
+
client.executeSubscription<T, V>(request.value, {
+
...args.context,
+
}),
+
share
+
);
+
} else {
+
source.value = undefined;
+
}
+
}, watchOptions)
+
);
-
watchEffect(onInvalidate => {
-
if (source.value) {
-
onInvalidate(
-
pipe(
-
source.value,
-
onStart(() => {
-
fetching.value = true;
-
}),
-
onEnd(() => {
-
fetching.value = false;
-
}),
-
onPush(result => {
-
fetching.value = true;
-
(data.value =
-
result.data !== undefined
-
? typeof scanHandler.value === 'function'
-
? scanHandler.value(data.value as any, result.data!)
-
: result.data
-
: (result.data as any)),
-
(error.value = result.error);
-
extensions.value = result.extensions;
-
stale.value = !!result.stale;
-
operation.value = result.operation;
-
}),
-
publish
-
).unsubscribe
-
);
-
}
-
}, watchOptions);
+
stops.push(
+
watchEffect(onInvalidate => {
+
if (source.value) {
+
onInvalidate(
+
pipe(
+
source.value,
+
onStart(() => {
+
fetching.value = true;
+
}),
+
onEnd(() => {
+
fetching.value = false;
+
}),
+
onPush(result => {
+
fetching.value = true;
+
(data.value =
+
result.data !== undefined
+
? typeof scanHandler.value === 'function'
+
? scanHandler.value(data.value as any, result.data!)
+
: result.data
+
: (result.data as any)),
+
(error.value = result.error);
+
extensions.value = result.extensions;
+
stale.value = !!result.stale;
+
operation.value = result.operation;
+
}),
+
publish
+
).unsubscribe
+
);
+
}
+
}, watchOptions)
+
);
const state: UseSubscriptionState<T, R, V> = {
data,
+1 -1
yarn.lock
···
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
-
vue@^3.0.2:
+
vue@^3.0.11:
version "3.0.11"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.0.11.tgz#c82f9594cbf4dcc869241d4c8dd3e08d9a8f4b5f"
integrity sha512-3/eUi4InQz8MPzruHYSTQPxtM3LdZ1/S/BvaU021zBnZi0laRUyH6pfuE4wtUeLvI8wmUNwj5wrZFvbHUXL9dw==