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

Configure Feed

Select the types of activity you want to include in your feed.

feat(vue): refactor composable functions (#3619)

authored by

Yurk Sha and committed by
GitHub
068df71f 9e674d3e

+663 -376
+5
.changeset/chatty-mice-join.md
··· 1 + --- 2 + '@urql/vue': minor 3 + --- 4 + 5 + Refactor composable functions with a focus on avoiding memory leaks and Vue best practices
+3 -4
packages/vue-urql/src/useClientHandle.ts
··· 1 - import type { DocumentNode } from 'graphql'; 2 - import type { AnyVariables, Client, TypedDocumentNode } from '@urql/core'; 1 + import type { AnyVariables, Client, DocumentInput } from '@urql/core'; 3 2 import type { WatchStopHandle } from 'vue'; 4 3 import { getCurrentInstance, onMounted, onBeforeUnmount } from 'vue'; 5 4 ··· 75 74 * function or when chained in an `async setup()` function. 76 75 */ 77 76 useMutation<T = any, V extends AnyVariables = AnyVariables>( 78 - query: TypedDocumentNode<T, V> | DocumentNode | string 77 + query: DocumentInput<T, V> 79 78 ): UseMutationResponse<T, V>; 80 79 } 81 80 ··· 153 152 }, 154 153 155 154 useMutation<T = any, V extends AnyVariables = AnyVariables>( 156 - query: TypedDocumentNode<T, V> | DocumentNode | string 155 + query: DocumentInput<T, V> 157 156 ): UseMutationResponse<T, V> { 158 157 return callUseMutation(query, client); 159 158 },
+16 -18
packages/vue-urql/src/useMutation.test.ts
··· 1 1 import { OperationResult, OperationResultSource } from '@urql/core'; 2 - import { reactive } from 'vue'; 2 + import { readonly } from 'vue'; 3 3 import { vi, expect, it, beforeEach, describe } from 'vitest'; 4 4 5 5 vi.mock('./useClient.ts', async () => { ··· 30 30 () => subject.source as OperationResultSource<OperationResult> 31 31 ); 32 32 33 - const mutation = reactive( 34 - useMutation(gql` 35 - mutation { 36 - test 37 - } 38 - `) 39 - ); 33 + const mutation = useMutation(gql` 34 + mutation { 35 + test 36 + } 37 + `); 40 38 41 - expect(mutation).toMatchObject({ 39 + expect(readonly(mutation)).toMatchObject({ 42 40 data: undefined, 43 41 stale: false, 44 42 fetching: false, ··· 50 48 51 49 const promise = mutation.executeMutation({ test: true }); 52 50 53 - expect(mutation.fetching).toBe(true); 54 - expect(mutation.stale).toBe(false); 55 - expect(mutation.error).toBe(undefined); 51 + expect(mutation.fetching.value).toBe(true); 52 + expect(mutation.stale.value).toBe(false); 53 + expect(mutation.error.value).toBe(undefined); 56 54 57 55 expect(clientMutation).toHaveBeenCalledTimes(1); 58 56 59 57 subject.next({ data: { test: true }, stale: false }); 60 - await promise.then(function () { 61 - expect(mutation.fetching).toBe(false); 62 - expect(mutation.stale).toBe(false); 63 - expect(mutation.error).toBe(undefined); 64 - expect(mutation.data).toEqual({ test: true }); 65 - }); 58 + 59 + await promise; 60 + expect(mutation.fetching.value).toBe(false); 61 + expect(mutation.stale.value).toBe(false); 62 + expect(mutation.error.value).toBe(undefined); 63 + expect(mutation.data.value).toHaveProperty('test', true); 66 64 }); 67 65 });
+11 -13
packages/vue-urql/src/useMutation.ts
··· 1 1 /* eslint-disable react-hooks/rules-of-hooks */ 2 2 3 3 import type { Ref } from 'vue'; 4 - import { ref, shallowRef } from 'vue'; 5 - import type { DocumentNode } from 'graphql'; 4 + import { ref } from 'vue'; 6 5 import { pipe, onPush, filter, toPromise, take } from 'wonka'; 7 6 8 7 import type { 9 8 Client, 10 9 AnyVariables, 11 - TypedDocumentNode, 12 10 CombinedError, 13 11 Operation, 14 12 OperationContext, 15 13 OperationResult, 14 + DocumentInput, 16 15 } from '@urql/core'; 17 - import { createRequest } from '@urql/core'; 18 16 19 17 import { useClient } from './useClient'; 20 18 import type { MaybeRef } from './utils'; 21 - import { unref } from './utils'; 19 + import { createRequestWithArgs, useRequestState } from './utils'; 22 20 23 21 /** State of the last mutation executed by {@link useMutation}. 24 22 * ··· 126 124 * ``` 127 125 */ 128 126 export function useMutation<T = any, V extends AnyVariables = AnyVariables>( 129 - query: TypedDocumentNode<T, V> | DocumentNode | string 127 + query: DocumentInput<T, V> 130 128 ): UseMutationResponse<T, V> { 131 129 return callUseMutation(query); 132 130 } 133 131 134 132 export function callUseMutation<T = any, V extends AnyVariables = AnyVariables>( 135 - query: MaybeRef<TypedDocumentNode<T, V> | DocumentNode | string>, 133 + query: MaybeRef<DocumentInput<T, V>>, 136 134 client: Ref<Client> = useClient() 137 135 ): UseMutationResponse<T, V> { 138 136 const data: Ref<T | undefined> = ref(); 139 - const stale: Ref<boolean> = ref(false); 140 - const fetching: Ref<boolean> = ref(false); 141 - const error: Ref<CombinedError | undefined> = shallowRef(); 142 - const operation: Ref<Operation<T, V> | undefined> = shallowRef(); 143 - const extensions: Ref<Record<string, any> | undefined> = shallowRef(); 137 + 138 + const { fetching, operation, extensions, stale, error } = useRequestState< 139 + T, 140 + V 141 + >(); 144 142 145 143 return { 146 144 data, ··· 157 155 158 156 return pipe( 159 157 client.value.executeMutation<T, V>( 160 - createRequest<T, V>(unref(query), unref(variables)), 158 + createRequestWithArgs({ query, variables }), 161 159 context || {} 162 160 ), 163 161 onPush(result => {
+322 -44
packages/vue-urql/src/useQuery.test.ts
··· 1 - import { OperationResult, OperationResultSource } from '@urql/core'; 2 - import { nextTick, reactive, ref } from 'vue'; 1 + import { 2 + OperationResult, 3 + OperationResultSource, 4 + RequestPolicy, 5 + } from '@urql/core'; 6 + import { computed, nextTick, reactive, readonly, ref } from 'vue'; 3 7 import { vi, expect, it, describe } from 'vitest'; 4 8 5 9 vi.mock('./useClient.ts', async () => ({ ··· 10 14 11 15 import { pipe, makeSubject, fromValue, delay } from 'wonka'; 12 16 import { createClient } from '@urql/core'; 13 - import { useQuery } from './useQuery'; 17 + import { useQuery, UseQueryArgs } from './useQuery'; 14 18 15 19 const client = createClient({ url: '/graphql', exchanges: [] }); 16 20 21 + const createQuery = (args: UseQueryArgs) => { 22 + const executeQuery = vi 23 + .spyOn(client, 'executeQuery') 24 + .mockImplementation(request => { 25 + return pipe( 26 + fromValue({ operation: request, data: { test: true } }), 27 + delay(1) 28 + ) as any; 29 + }); 30 + 31 + const query$ = useQuery(args); 32 + 33 + return { 34 + query$, 35 + executeQuery, 36 + }; 37 + }; 38 + 17 39 describe('useQuery', () => { 18 40 it('runs a query and updates data', async () => { 19 41 const subject = makeSubject<any>(); ··· 23 45 () => subject.source as OperationResultSource<OperationResult> 24 46 ); 25 47 26 - const _query = useQuery({ 48 + const query = useQuery({ 27 49 query: `{ test }`, 28 50 }); 29 - const query = reactive(_query); 30 51 31 - expect(query).toMatchObject({ 52 + expect(readonly(query)).toMatchObject({ 32 53 data: undefined, 33 54 stale: false, 34 55 fetching: true, ··· 54 75 } 55 76 ); 56 77 57 - expect(query.fetching).toBe(true); 78 + expect(query.fetching.value).toBe(true); 58 79 59 80 subject.next({ data: { test: true } }); 60 81 61 - expect(query.fetching).toBe(false); 62 - expect(query.data).toEqual({ test: true }); 82 + expect(query.fetching.value).toBe(false); 83 + expect(query.data.value).toHaveProperty('test', true); 63 84 }); 64 85 65 86 it('runs queries as a promise-like that resolves when used', async () => { ··· 79 100 }); 80 101 81 102 it('runs queries as a promise-like that resolves even when the query changes', async () => { 82 - const executeQuery = vi 83 - .spyOn(client, 'executeQuery') 84 - .mockImplementation(request => { 85 - return pipe( 86 - fromValue({ operation: request, data: { test: true } }), 87 - delay(1) 88 - ) as any; 89 - }); 90 - 91 103 const doc = ref('{ test }'); 92 104 93 - const query$ = useQuery({ 105 + const { executeQuery, query$ } = createQuery({ 94 106 query: doc, 95 107 }); 96 108 ··· 108 120 ); 109 121 }); 110 122 111 - it('reacts to variables changing', async () => { 112 - const executeQuery = vi 113 - .spyOn(client, 'executeQuery') 114 - .mockImplementation(request => { 115 - return pipe( 116 - fromValue({ operation: request, data: { test: true } }), 117 - delay(1) 118 - ) as any; 119 - }); 123 + it('runs a query with different variables', async () => { 124 + const simpleVariables = { 125 + null: null, 126 + NaN: NaN, 127 + empty: '', 128 + bool: false, 129 + int: 1, 130 + float: 1.1, 131 + string: 'string', 132 + blob: new Blob(), 133 + date: new Date(), 134 + }; 120 135 121 - const variables = { 122 - test: ref(1), 136 + const variablesSet = { 137 + func: () => 'func', 138 + ref: ref('ref'), 139 + computed: computed(() => 'computed'), 140 + ...simpleVariables, 141 + }; 142 + 143 + const variablesSetUnwrapped = { 144 + func: 'func', 145 + ref: 'ref', 146 + computed: 'computed', 147 + ...simpleVariables, 123 148 }; 124 - const query$ = useQuery({ 125 - query: '{ test }', 149 + 150 + const { query$ } = createQuery({ 151 + query: ref('{ test }'), 152 + variables: { 153 + ...variablesSet, 154 + nested: variablesSet, 155 + array: [variablesSet], 156 + }, 157 + }); 158 + 159 + await query$; 160 + 161 + expect(query$.operation.value?.variables).toStrictEqual({ 162 + ...variablesSetUnwrapped, 163 + nested: variablesSetUnwrapped, 164 + array: [variablesSetUnwrapped], 165 + }); 166 + }); 167 + 168 + it('reacts to ref variables changing', async () => { 169 + const variables = ref({ prop: 1 }); 170 + 171 + const { executeQuery, query$ } = createQuery({ 172 + query: ref('{ test }'), 126 173 variables, 127 174 }); 128 175 129 176 await query$; 177 + expect(executeQuery).toHaveBeenCalledTimes(1); 178 + expect(query$.operation.value).toHaveProperty('variables.prop', 1); 130 179 180 + variables.value.prop++; 181 + await query$; 182 + expect(executeQuery).toHaveBeenCalledTimes(2); 183 + expect(query$.operation.value).toHaveProperty('variables.prop', 2); 184 + 185 + variables.value = { prop: 3 }; 186 + await query$; 187 + expect(executeQuery).toHaveBeenCalledTimes(3); 188 + expect(query$.operation.value).toHaveProperty('variables.prop', 3); 189 + }); 190 + 191 + it('reacts to nested ref variables changing', async () => { 192 + const prop = ref(1); 193 + 194 + const { executeQuery, query$ } = createQuery({ 195 + query: ref('{ test }'), 196 + variables: { prop }, 197 + }); 198 + 199 + await query$; 131 200 expect(executeQuery).toHaveBeenCalledTimes(1); 201 + expect(query$.operation.value).toHaveProperty('variables.prop', 1); 132 202 133 - expect(query$.operation.value).toHaveProperty('variables.test', 1); 203 + prop.value++; 204 + await query$; 205 + expect(executeQuery).toHaveBeenCalledTimes(2); 206 + expect(query$.operation.value).toHaveProperty('variables.prop', 2); 207 + }); 134 208 135 - variables.test.value = 2; 209 + it('reacts to deep nested ref variables changing', async () => { 210 + const prop = ref(1); 211 + 212 + const { executeQuery, query$ } = createQuery({ 213 + query: ref('{ test }'), 214 + variables: { deep: { nested: { prop } } }, 215 + }); 136 216 137 217 await query$; 218 + expect(executeQuery).toHaveBeenCalledTimes(1); 219 + expect(query$.operation.value).toHaveProperty( 220 + 'variables.deep.nested.prop', 221 + 1 222 + ); 138 223 224 + prop.value++; 225 + await query$; 139 226 expect(executeQuery).toHaveBeenCalledTimes(2); 140 - expect(query$.operation.value).toHaveProperty('variables.test', 2); 227 + expect(query$.operation.value).toHaveProperty( 228 + 'variables.deep.nested.prop', 229 + 2 230 + ); 231 + }); 232 + 233 + it('reacts to reactive variables changing', async () => { 234 + const prop = ref(1); 235 + const variables = reactive({ prop: 1, deep: { nested: { prop } } }); 236 + 237 + const { executeQuery, query$ } = createQuery({ 238 + query: ref('{ test }'), 239 + variables, 240 + }); 241 + 242 + await query$; 243 + expect(executeQuery).toHaveBeenCalledTimes(1); 244 + expect(query$.operation.value).toHaveProperty('variables.prop', 1); 245 + 246 + variables.prop++; 247 + await query$; 248 + expect(executeQuery).toHaveBeenCalledTimes(2); 249 + expect(query$.operation.value).toHaveProperty('variables.prop', 2); 250 + 251 + prop.value++; 252 + await query$; 253 + expect(executeQuery).toHaveBeenCalledTimes(3); 254 + expect(query$.operation.value).toHaveProperty( 255 + 'variables.deep.nested.prop', 256 + 2 257 + ); 258 + }); 259 + 260 + it('reacts to computed variables changing', async () => { 261 + const prop = ref(1); 262 + const prop2 = ref(1); 263 + const variables = computed(() => ({ 264 + prop: prop.value, 265 + deep: { nested: { prop2 } }, 266 + })); 267 + 268 + const { executeQuery, query$ } = createQuery({ 269 + query: ref('{ test }'), 270 + variables, 271 + }); 272 + 273 + await query$; 274 + expect(executeQuery).toHaveBeenCalledTimes(1); 275 + expect(query$.operation.value).toHaveProperty('variables.prop', 1); 276 + 277 + prop.value++; 278 + await query$; 279 + expect(executeQuery).toHaveBeenCalledTimes(2); 280 + expect(query$.operation.value).toHaveProperty('variables.prop', 2); 281 + 282 + prop2.value++; 283 + await query$; 284 + expect(executeQuery).toHaveBeenCalledTimes(3); 285 + expect(query$.operation.value).toHaveProperty( 286 + 'variables.deep.nested.prop2', 287 + 2 288 + ); 289 + }); 290 + 291 + it('reacts to callback variables changing', async () => { 292 + const prop = ref(1); 293 + const prop2 = ref(1); 294 + const variables = () => ({ 295 + prop: prop.value, 296 + deep: { nested: { prop2 } }, 297 + }); 298 + 299 + const { executeQuery, query$ } = createQuery({ 300 + query: ref('{ test }'), 301 + variables, 302 + }); 303 + 304 + await query$; 305 + expect(executeQuery).toHaveBeenCalledTimes(1); 306 + expect(query$.operation.value).toHaveProperty('variables.prop', 1); 307 + 308 + prop.value++; 309 + await query$; 310 + expect(executeQuery).toHaveBeenCalledTimes(2); 311 + expect(query$.operation.value).toHaveProperty('variables.prop', 2); 312 + 313 + prop2.value++; 314 + await query$; 315 + expect(executeQuery).toHaveBeenCalledTimes(3); 316 + expect(query$.operation.value).toHaveProperty( 317 + 'variables.deep.nested.prop2', 318 + 2 319 + ); 320 + }); 321 + 322 + it('reacts to reactive context argument', async () => { 323 + const context = ref<{ requestPolicy: RequestPolicy }>({ 324 + requestPolicy: 'cache-only', 325 + }); 326 + 327 + const { executeQuery, query$ } = createQuery({ 328 + query: ref('{ test }'), 329 + context, 330 + }); 331 + 332 + await query$; 333 + expect(executeQuery).toHaveBeenCalledTimes(1); 334 + 335 + context.value.requestPolicy = 'network-only'; 336 + await query$; 337 + expect(executeQuery).toHaveBeenCalledTimes(2); 338 + }); 339 + 340 + it('reacts to callback context argument', async () => { 341 + const requestPolicy = ref<RequestPolicy>('cache-only'); 342 + 343 + const { executeQuery, query$ } = createQuery({ 344 + query: ref('{ test }'), 345 + context: () => ({ 346 + requestPolicy: requestPolicy.value, 347 + }), 348 + }); 349 + 350 + await query$; 351 + expect(executeQuery).toHaveBeenCalledTimes(1); 352 + 353 + requestPolicy.value = 'network-only'; 354 + await query$; 355 + expect(executeQuery).toHaveBeenCalledTimes(2); 141 356 }); 142 357 143 358 it('pauses query when asked to do so', async () => { ··· 148 363 () => subject.source as OperationResultSource<OperationResult> 149 364 ); 150 365 366 + const query = useQuery({ 367 + query: `{ test }`, 368 + pause: true, 369 + }); 370 + 371 + expect(executeQuery).not.toHaveBeenCalled(); 372 + 373 + query.resume(); 374 + await nextTick(); 375 + expect(query.fetching.value).toBe(true); 376 + 377 + subject.next({ data: { test: true } }); 378 + 379 + expect(query.fetching.value).toBe(false); 380 + expect(query.data.value).toHaveProperty('test', true); 381 + }); 382 + 383 + it('pauses query with ref variable', async () => { 151 384 const pause = ref(true); 152 385 153 - const _query = useQuery({ 154 - query: `{ test }`, 386 + const { executeQuery, query$ } = createQuery({ 387 + query: ref('{ test }'), 388 + pause, 389 + }); 390 + 391 + await query$; 392 + expect(executeQuery).not.toHaveBeenCalled(); 393 + 394 + pause.value = false; 395 + await query$; 396 + expect(executeQuery).toHaveBeenCalledTimes(1); 397 + 398 + query$.pause(); 399 + query$.resume(); 400 + await query$; 401 + expect(executeQuery).toHaveBeenCalledTimes(2); 402 + }); 403 + 404 + it('pauses query with computed variable', async () => { 405 + const pause = ref(true); 406 + 407 + const { executeQuery, query$ } = createQuery({ 408 + query: ref('{ test }'), 409 + pause: computed(() => pause.value), 410 + }); 411 + 412 + await query$; 413 + expect(executeQuery).not.toHaveBeenCalled(); 414 + 415 + pause.value = false; 416 + await query$; 417 + expect(executeQuery).toHaveBeenCalledTimes(1); 418 + 419 + query$.pause(); 420 + query$.resume(); 421 + await query$; 422 + // this shouldn't be called, as pause/resume functionality should works in sync with passed `pause` variable, e.g.: 423 + // if we pass readonly computed variable, then we want to make sure that its value fully controls the state of the request. 424 + expect(executeQuery).toHaveBeenCalledTimes(1); 425 + }); 426 + 427 + it('pauses query with callback', async () => { 428 + const pause = ref(true); 429 + 430 + const { executeQuery, query$ } = createQuery({ 431 + query: ref('{ test }'), 155 432 pause: () => pause.value, 156 433 }); 157 - const query = reactive(_query); 158 434 435 + await query$; 159 436 expect(executeQuery).not.toHaveBeenCalled(); 160 437 161 438 pause.value = false; 162 - await nextTick(); 163 - expect(query.fetching).toBe(true); 439 + await query$; 440 + expect(executeQuery).toHaveBeenCalledTimes(1); 164 441 165 - subject.next({ data: { test: true } }); 166 - 167 - expect(query.fetching).toBe(false); 168 - expect(query.data).toEqual({ test: true }); 442 + query$.pause(); 443 + query$.resume(); 444 + await query$; 445 + // the same as computed variable example - user has full control over the request state if using callback 446 + expect(executeQuery).toHaveBeenCalledTimes(1); 169 447 }); 170 448 });
+76 -146
packages/vue-urql/src/useQuery.ts
··· 1 1 /* eslint-disable react-hooks/rules-of-hooks */ 2 2 3 3 import type { Ref, WatchStopHandle } from 'vue'; 4 - import { isRef, ref, shallowRef, watch, watchEffect, reactive } from 'vue'; 4 + import { ref, watchEffect } from 'vue'; 5 5 6 - import type { Subscription, Source } from 'wonka'; 6 + import type { Subscription } from 'wonka'; 7 7 import { pipe, subscribe, onEnd } from 'wonka'; 8 8 9 9 import type { 10 10 Client, 11 11 AnyVariables, 12 - OperationResult, 13 12 GraphQLRequestParams, 14 13 CombinedError, 15 14 OperationContext, 16 15 RequestPolicy, 17 16 Operation, 18 17 } from '@urql/core'; 19 - import { createRequest } from '@urql/core'; 20 18 21 19 import { useClient } from './useClient'; 22 20 23 21 import type { MaybeRef, MaybeRefObj } from './utils'; 24 - import { unref, updateShallowRef } from './utils'; 22 + import { useRequestState, useClientState } from './utils'; 25 23 26 24 /** Input arguments for the {@link useQuery} function. 27 25 * ··· 195 193 V extends AnyVariables = AnyVariables, 196 194 > = UseQueryState<T, V> & PromiseLike<UseQueryState<T, V>>; 197 195 198 - const watchOptions = { 199 - flush: 'pre' as const, 200 - }; 201 - 202 196 /** Function to run a GraphQL query and get reactive GraphQL results. 203 197 * 204 198 * @param args - a {@link UseQueryArgs} object, to pass a `query`, `variables`, and options. ··· 241 235 } 242 236 243 237 export function callUseQuery<T = any, V extends AnyVariables = AnyVariables>( 244 - _args: UseQueryArgs<T, V>, 238 + args: UseQueryArgs<T, V>, 245 239 client: Ref<Client> = useClient(), 246 - stops: WatchStopHandle[] = [] 240 + stops?: WatchStopHandle[] 247 241 ): UseQueryResponse<T, V> { 248 - const args = reactive(_args) as UseQueryArgs<T, V>; 249 - 250 242 const data: Ref<T | undefined> = ref(); 251 - const stale: Ref<boolean> = ref(false); 252 - const fetching: Ref<boolean> = ref(false); 253 - const error: Ref<CombinedError | undefined> = shallowRef(); 254 - const operation: Ref<Operation<T, V> | undefined> = shallowRef(); 255 - const extensions: Ref<Record<string, any> | undefined> = shallowRef(); 256 243 257 - const isPaused: Ref<boolean> = ref(!!unref(args.pause)); 258 - if (isRef(args.pause) || typeof args.pause === 'function') { 259 - stops.push(watch(args.pause, value => (isPaused.value = value))); 260 - } 244 + const { fetching, operation, extensions, stale, error } = useRequestState< 245 + T, 246 + V 247 + >(); 261 248 262 - const input = shallowRef({ 263 - request: createRequest<T, V>(unref(args.query), unref(args.variables) as V), 264 - requestPolicy: unref(args.requestPolicy), 265 - isPaused: isPaused.value, 266 - }); 249 + const { isPaused, source, pause, resume, execute, teardown } = useClientState( 250 + args, 251 + client, 252 + 'executeQuery' 253 + ); 267 254 268 - const source: Ref<Source<OperationResult<T, V>> | undefined> = ref(); 255 + const teardownQuery = watchEffect( 256 + onInvalidate => { 257 + if (source.value) { 258 + fetching.value = true; 259 + stale.value = false; 269 260 270 - stops.push( 271 - watchEffect(() => { 272 - updateShallowRef(input, { 273 - request: createRequest<T, V>( 274 - unref(args.query), 275 - unref(args.variables) as V 276 - ), 277 - requestPolicy: unref(args.requestPolicy), 278 - isPaused: isPaused.value, 279 - }); 280 - }, watchOptions) 261 + onInvalidate( 262 + pipe( 263 + source.value, 264 + onEnd(() => { 265 + fetching.value = false; 266 + stale.value = false; 267 + }), 268 + subscribe(res => { 269 + data.value = res.data; 270 + stale.value = !!res.stale; 271 + fetching.value = false; 272 + error.value = res.error; 273 + operation.value = res.operation; 274 + extensions.value = res.extensions; 275 + }) 276 + ).unsubscribe 277 + ); 278 + } else { 279 + fetching.value = false; 280 + stale.value = false; 281 + } 282 + }, 283 + { 284 + // NOTE: This part of the query pipeline is only initialised once and will need 285 + // to do so synchronously 286 + flush: 'sync', 287 + } 281 288 ); 282 289 283 - stops.push( 284 - watchEffect(() => { 285 - source.value = !input.value.isPaused 286 - ? client.value.executeQuery<T, V>(input.value.request, { 287 - requestPolicy: unref(args.requestPolicy), 288 - ...unref(args.context), 289 - }) 290 - : undefined; 291 - }, watchOptions) 292 - ); 290 + stops && stops.push(teardown, teardownQuery); 291 + 292 + const then: UseQueryResponse<T, V>['then'] = (onFulfilled, onRejected) => { 293 + let sub: Subscription | void; 294 + 295 + const promise = new Promise<UseQueryState<T, V>>(resolve => { 296 + if (!source.value) { 297 + return resolve(state); 298 + } 299 + let hasResult = false; 300 + sub = pipe( 301 + source.value, 302 + subscribe(() => { 303 + if (!state.fetching.value && !state.stale.value) { 304 + if (sub) sub.unsubscribe(); 305 + hasResult = true; 306 + resolve(state); 307 + } 308 + }) 309 + ); 310 + if (hasResult) sub.unsubscribe(); 311 + }); 312 + 313 + return promise.then(onFulfilled, onRejected); 314 + }; 293 315 294 316 const state: UseQueryState<T, V> = { 295 317 data, ··· 299 321 extensions, 300 322 fetching, 301 323 isPaused, 324 + pause, 325 + resume, 302 326 executeQuery(opts?: Partial<OperationContext>): UseQueryResponse<T, V> { 303 - const s = (source.value = client.value.executeQuery<T, V>( 304 - input.value.request, 305 - { 306 - requestPolicy: unref(args.requestPolicy), 307 - ...unref(args.context), 308 - ...opts, 309 - } 310 - )); 311 - 312 - return { 313 - ...response, 314 - then(onFulfilled, onRejected) { 315 - let sub: Subscription | void; 316 - return new Promise<UseQueryState<T, V>>(resolve => { 317 - let hasResult = false; 318 - sub = pipe( 319 - s, 320 - subscribe(() => { 321 - if (!state.fetching.value && !state.stale.value) { 322 - if (sub) sub.unsubscribe(); 323 - hasResult = true; 324 - resolve(state); 325 - } 326 - }) 327 - ); 328 - if (hasResult) sub.unsubscribe(); 329 - }).then(onFulfilled, onRejected); 330 - }, 331 - }; 332 - }, 333 - pause() { 334 - isPaused.value = true; 335 - }, 336 - resume() { 337 - isPaused.value = false; 327 + execute(opts); 328 + return { ...state, then }; 338 329 }, 339 330 }; 340 331 341 - stops.push( 342 - watchEffect( 343 - onInvalidate => { 344 - if (source.value) { 345 - fetching.value = true; 346 - stale.value = false; 347 - 348 - onInvalidate( 349 - pipe( 350 - source.value, 351 - onEnd(() => { 352 - fetching.value = false; 353 - stale.value = false; 354 - }), 355 - subscribe(res => { 356 - data.value = res.data; 357 - stale.value = !!res.stale; 358 - fetching.value = false; 359 - error.value = res.error; 360 - operation.value = res.operation; 361 - extensions.value = res.extensions; 362 - }) 363 - ).unsubscribe 364 - ); 365 - } else { 366 - fetching.value = false; 367 - stale.value = false; 368 - } 369 - }, 370 - { 371 - // NOTE: This part of the query pipeline is only initialised once and will need 372 - // to do so synchronously 373 - flush: 'sync', 374 - } 375 - ) 376 - ); 377 - 378 - const response: UseQueryResponse<T, V> = { 379 - ...state, 380 - then(onFulfilled, onRejected) { 381 - let sub: Subscription | void; 382 - const promise = new Promise<UseQueryState<T, V>>(resolve => { 383 - if (!source.value) return resolve(state); 384 - let hasResult = false; 385 - sub = pipe( 386 - source.value, 387 - subscribe(() => { 388 - if (!state.fetching.value && !state.stale.value) { 389 - if (sub) sub.unsubscribe(); 390 - hasResult = true; 391 - resolve(state); 392 - } 393 - }) 394 - ); 395 - if (hasResult) sub.unsubscribe(); 396 - }); 397 - 398 - return promise.then(onFulfilled, onRejected); 399 - }, 400 - }; 401 - 402 - return response; 332 + return { ...state, then }; 403 333 }
+25 -28
packages/vue-urql/src/useSubscription.test.ts
··· 1 1 // @vitest-environment jsdom 2 2 3 3 import { OperationResult, OperationResultSource } from '@urql/core'; 4 - import { nextTick, reactive, ref } from 'vue'; 4 + import { nextTick, readonly, ref } from 'vue'; 5 5 import { vi, expect, it, describe } from 'vitest'; 6 6 7 7 vi.mock('./useClient.ts', async () => ({ ··· 25 25 () => subject.source as OperationResultSource<OperationResult> 26 26 ); 27 27 28 - const sub = reactive( 29 - useSubscription({ 30 - query: `{ test }`, 31 - }) 32 - ); 28 + const sub = useSubscription({ 29 + query: `{ test }`, 30 + }); 33 31 34 - expect(sub).toMatchObject({ 32 + expect(readonly(sub)).toMatchObject({ 35 33 data: undefined, 36 34 stale: false, 37 35 fetching: true, ··· 53 51 expect.any(Object) 54 52 ); 55 53 56 - expect(sub.fetching).toBe(true); 54 + expect(sub.fetching.value).toBe(true); 57 55 58 56 subject.next({ data: { test: true } }); 59 - expect(sub.data).toEqual({ test: true }); 57 + expect(sub.data.value).toHaveProperty('test', true); 58 + 60 59 subject.complete(); 61 - expect(sub.fetching).toBe(false); 60 + expect(sub.fetching.value).toBe(false); 62 61 }); 63 62 64 63 it('updates the executed subscription when inputs change', async () => { ··· 70 69 ); 71 70 72 71 const variables = ref({}); 73 - const sub = reactive( 74 - useSubscription({ 75 - query: `{ test }`, 76 - variables, 77 - }) 78 - ); 72 + const sub = useSubscription({ 73 + query: `{ test }`, 74 + variables, 75 + }); 79 76 80 77 expect(executeSubscription).toHaveBeenCalledWith( 81 78 { ··· 87 84 ); 88 85 89 86 subject.next({ data: { test: true } }); 90 - expect(sub.data).toEqual({ test: true }); 87 + expect(sub.data.value).toHaveProperty('test', true); 91 88 92 89 variables.value = { test: true }; 93 90 await nextTick(); ··· 101 98 expect.any(Object) 102 99 ); 103 100 104 - expect(sub.fetching).toBe(true); 105 - expect(sub.data).toEqual({ test: true }); 101 + expect(sub.fetching.value).toBe(true); 102 + expect(sub.data.value).toHaveProperty('test', true); 106 103 }); 104 + 107 105 it('supports a custom scanning handler', async () => { 108 106 const subject = makeSubject<any>(); 109 107 const executeSubscription = vi ··· 115 113 const scanHandler = (currentState: any, nextState: any) => ({ 116 114 counter: (currentState ? currentState.counter : 0) + nextState.counter, 117 115 }); 118 - const sub = reactive( 119 - useSubscription( 120 - { 121 - query: `subscription { counter }`, 122 - }, 123 - scanHandler 124 - ) 116 + 117 + const sub = useSubscription( 118 + { 119 + query: `subscription { counter }`, 120 + }, 121 + scanHandler 125 122 ); 126 123 127 124 expect(executeSubscription).toHaveBeenCalledWith( ··· 134 131 ); 135 132 136 133 subject.next({ data: { counter: 1 } }); 137 - expect(sub.data).toEqual({ counter: 1 }); 134 + expect(sub.data.value).toHaveProperty('counter', 1); 138 135 139 136 subject.next({ data: { counter: 2 } }); 140 - expect(sub.data).toEqual({ counter: 3 }); 137 + expect(sub.data.value).toHaveProperty('counter', 3); 141 138 }); 142 139 });
+51 -95
packages/vue-urql/src/useSubscription.ts
··· 1 1 /* eslint-disable react-hooks/rules-of-hooks */ 2 2 3 - import type { Source } from 'wonka'; 4 3 import { pipe, subscribe, onEnd } from 'wonka'; 5 4 6 5 import type { Ref, WatchStopHandle } from 'vue'; 7 - import { isRef, ref, shallowRef, watch, watchEffect, reactive } from 'vue'; 6 + import { isRef, ref, watchEffect } from 'vue'; 8 7 9 8 import type { 10 9 Client, 11 10 GraphQLRequestParams, 12 11 AnyVariables, 13 - OperationResult, 14 12 CombinedError, 15 13 OperationContext, 16 14 Operation, 17 15 } from '@urql/core'; 18 - import { createRequest } from '@urql/core'; 19 16 20 17 import { useClient } from './useClient'; 21 18 22 19 import type { MaybeRef, MaybeRefObj } from './utils'; 23 - import { unref, updateShallowRef } from './utils'; 20 + import { useRequestState, useClientState } from './utils'; 24 21 25 22 /** Input arguments for the {@link useSubscription} function. 26 23 * ··· 89 86 export type SubscriptionHandler<T, R> = (prev: R | undefined, data: T) => R; 90 87 91 88 /** A {@link SubscriptionHandler} or a reactive ref of one. */ 92 - export type SubscriptionHandlerArg<T, R> = MaybeRef<SubscriptionHandler<T, R>>; 89 + export type SubscriptionHandlerArg<T, R> = 90 + | Ref<SubscriptionHandler<T, R>> 91 + | SubscriptionHandler<T, R>; 93 92 94 93 /** State of the current query, your {@link useSubscription} function is executing. 95 94 * ··· 182 181 executeSubscription(opts?: Partial<OperationContext>): void; 183 182 } 184 183 185 - const watchOptions = { 186 - flush: 'pre' as const, 187 - }; 188 - 189 184 /** Function to run a GraphQL subscription and get reactive GraphQL results. 190 185 * 191 186 * @param args - a {@link UseSubscriptionArgs} object, to pass a `query`, `variables`, and options. ··· 229 224 V extends AnyVariables = AnyVariables, 230 225 >( 231 226 args: UseSubscriptionArgs<T, V>, 232 - handler?: MaybeRef<SubscriptionHandler<T, R>> 227 + handler?: SubscriptionHandlerArg<T, R> 233 228 ): UseSubscriptionResponse<T, R, V> { 234 229 return callUseSubscription(args, handler); 235 230 } ··· 239 234 R = T, 240 235 V extends AnyVariables = AnyVariables, 241 236 >( 242 - _args: UseSubscriptionArgs<T, V>, 243 - handler?: MaybeRef<SubscriptionHandler<T, R>>, 237 + args: UseSubscriptionArgs<T, V>, 238 + handler?: SubscriptionHandlerArg<T, R>, 244 239 client: Ref<Client> = useClient(), 245 - stops: WatchStopHandle[] = [] 240 + stops?: WatchStopHandle[] 246 241 ): UseSubscriptionResponse<T, R, V> { 247 - const args = reactive(_args) as UseSubscriptionArgs<T, V>; 248 - 249 242 const data: Ref<R | undefined> = ref(); 250 - const stale: Ref<boolean> = ref(false); 251 - const fetching: Ref<boolean> = ref(false); 252 - const error: Ref<CombinedError | undefined> = shallowRef(); 253 - const operation: Ref<Operation<T, V> | undefined> = shallowRef(); 254 - const extensions: Ref<Record<string, any> | undefined> = shallowRef(); 255 243 256 - const scanHandler = ref(handler); 257 - const isPaused: Ref<boolean> = ref(!!unref(args.pause)); 258 - if (isRef(args.pause) || typeof args.pause === 'function') { 259 - stops.push(watch(args.pause, value => (isPaused.value = value))); 260 - } 244 + const { fetching, operation, extensions, stale, error } = useRequestState< 245 + T, 246 + V 247 + >(); 261 248 262 - const input = shallowRef({ 263 - request: createRequest<T, V>(unref(args.query), unref(args.variables) as V), 264 - isPaused: isPaused.value, 265 - }); 249 + const { isPaused, source, pause, resume, execute, teardown } = useClientState( 250 + args, 251 + client, 252 + 'executeSubscription' 253 + ); 266 254 267 - const source: Ref<Source<OperationResult<T, V>> | undefined> = ref(); 255 + const teardownSubscription = watchEffect(onInvalidate => { 256 + if (source.value) { 257 + fetching.value = true; 268 258 269 - stops.push( 270 - watchEffect(() => { 271 - updateShallowRef(input, { 272 - request: createRequest<T, V>( 273 - unref(args.query), 274 - unref(args.variables) as V 275 - ), 276 - isPaused: isPaused.value, 277 - }); 278 - }, watchOptions) 279 - ); 259 + onInvalidate( 260 + pipe( 261 + source.value, 262 + onEnd(() => { 263 + fetching.value = false; 264 + }), 265 + subscribe(result => { 266 + fetching.value = true; 267 + error.value = result.error; 268 + extensions.value = result.extensions; 269 + stale.value = !!result.stale; 270 + operation.value = result.operation; 280 271 281 - stops.push( 282 - watchEffect(() => { 283 - source.value = !isPaused.value 284 - ? client.value.executeSubscription<T, V>(input.value.request, { 285 - ...unref(args.context), 272 + if (result.data != null && handler) { 273 + const cb = isRef(handler) ? handler.value : handler; 274 + if (typeof cb === 'function') { 275 + data.value = cb(data.value, result.data); 276 + return; 277 + } 278 + } 279 + data.value = result.data as R; 286 280 }) 287 - : undefined; 288 - }, watchOptions) 289 - ); 281 + ).unsubscribe 282 + ); 283 + } else { 284 + fetching.value = false; 285 + } 286 + }); 290 287 291 - stops.push( 292 - watchEffect(onInvalidate => { 293 - if (source.value) { 294 - fetching.value = true; 295 - 296 - onInvalidate( 297 - pipe( 298 - source.value, 299 - onEnd(() => { 300 - fetching.value = false; 301 - }), 302 - subscribe(result => { 303 - fetching.value = true; 304 - data.value = 305 - result.data != null 306 - ? typeof scanHandler.value === 'function' 307 - ? scanHandler.value(data.value as any, result.data) 308 - : result.data 309 - : (result.data as any); 310 - error.value = result.error; 311 - extensions.value = result.extensions; 312 - stale.value = !!result.stale; 313 - operation.value = result.operation; 314 - }) 315 - ).unsubscribe 316 - ); 317 - } else { 318 - fetching.value = false; 319 - } 320 - }, watchOptions) 321 - ); 288 + stops && stops.push(teardown, teardownSubscription); 322 289 323 290 const state: UseSubscriptionResponse<T, R, V> = { 324 291 data, ··· 328 295 extensions, 329 296 fetching, 330 297 isPaused, 298 + pause, 299 + resume, 331 300 executeSubscription( 332 301 opts?: Partial<OperationContext> 333 302 ): UseSubscriptionResponse<T, R, V> { 334 - source.value = client.value.executeSubscription<T, V>( 335 - input.value.request, 336 - { 337 - ...unref(args.context), 338 - ...opts, 339 - } 340 - ); 341 - 303 + execute(opts); 342 304 return state; 343 - }, 344 - pause() { 345 - isPaused.value = true; 346 - }, 347 - resume() { 348 - isPaused.value = false; 349 305 }, 350 306 }; 351 307
+154 -28
packages/vue-urql/src/utils.ts
··· 1 - import type { GraphQLRequest, AnyVariables } from '@urql/core'; 2 - import type { Ref, ShallowRef } from 'vue'; 3 - import { isRef } from 'vue'; 1 + import type { 2 + AnyVariables, 3 + Client, 4 + CombinedError, 5 + DocumentInput, 6 + Operation, 7 + OperationContext, 8 + OperationResult, 9 + OperationResultSource, 10 + } from '@urql/core'; 11 + import { createRequest } from '@urql/core'; 12 + import type { Ref } from 'vue'; 13 + import { watchEffect, isReadonly, computed, ref, shallowRef, isRef } from 'vue'; 14 + import type { UseSubscriptionArgs } from './useSubscription'; 15 + import type { UseQueryArgs } from './useQuery'; 4 16 5 17 export type MaybeRef<T> = T | (() => T) | Ref<T>; 6 18 export type MaybeRefObj<T> = T extends {} 7 19 ? { [K in keyof T]: MaybeRef<T[K]> } 8 20 : T; 9 21 10 - export const unref = <T>(maybeRef: MaybeRef<T>): T => 22 + const unwrap = <T>(maybeRef: MaybeRef<T>): T => 11 23 typeof maybeRef === 'function' 12 24 ? (maybeRef as () => T)() 13 25 : maybeRef != null && isRef(maybeRef) 14 26 ? maybeRef.value 15 27 : maybeRef; 16 28 17 - export interface RequestState< 18 - Data = any, 19 - Variables extends AnyVariables = AnyVariables, 20 - > { 21 - request: GraphQLRequest<Data, Variables>; 22 - isPaused: boolean; 23 - } 29 + const isPlainObject = (value: any): boolean => { 30 + if (typeof value !== 'object' || value === null) return false; 31 + return ( 32 + value.constructor && 33 + Object.getPrototypeOf(value).constructor === Object.prototype.constructor 34 + ); 35 + }; 36 + export const isArray = Array.isArray; 37 + 38 + const unwrapDeeply = <T>(input: T): T => { 39 + input = isRef(input) ? (input.value as T) : input; 40 + 41 + if (typeof input === 'function') { 42 + return unwrapDeeply(input()) as T; 43 + } 44 + 45 + if (input && typeof input === 'object') { 46 + if (isArray(input)) { 47 + const length = input.length; 48 + const out = new Array(length) as T; 49 + let i = 0; 50 + for (; i < length; i++) { 51 + out[i] = unwrapDeeply(input[i]); 52 + } 53 + 54 + return out; 55 + } else if (isPlainObject(input)) { 56 + const keys = Object.keys(input); 57 + const length = keys.length; 58 + let i = 0; 59 + let key: string; 60 + const out = {} as T; 24 61 25 - export function createRequestState< 26 - Data = any, 27 - Variables extends AnyVariables = AnyVariables, 28 - >( 29 - request: GraphQLRequest<Data, Variables>, 30 - isPaused: boolean 31 - ): RequestState<Data, Variables> { 32 - return { request, isPaused }; 33 - } 62 + for (; i < length; i++) { 63 + key = keys[i]; 64 + out[key] = unwrapDeeply(input[key]); 65 + } 34 66 35 - export const updateShallowRef = <T extends Record<string, any>>( 36 - ref: ShallowRef<T>, 37 - next: T 38 - ) => { 39 - for (const key in next) { 40 - if (ref.value[key] !== next[key]) { 41 - ref.value = next; 42 - return; 67 + return out; 43 68 } 44 69 } 70 + 71 + return input; 45 72 }; 73 + 74 + export const createRequestWithArgs = < 75 + T = any, 76 + V extends AnyVariables = AnyVariables, 77 + >( 78 + args: 79 + | UseQueryArgs<T, V> 80 + | UseSubscriptionArgs<T, V> 81 + | { query: MaybeRef<DocumentInput<T, V>>; variables: V } 82 + ) => { 83 + return createRequest<T, V>( 84 + unwrap(args.query), 85 + unwrapDeeply(args.variables) as V 86 + ); 87 + }; 88 + 89 + export const useRequestState = < 90 + T = any, 91 + V extends AnyVariables = AnyVariables, 92 + >() => { 93 + const stale: Ref<boolean> = ref(false); 94 + const fetching: Ref<boolean> = ref(false); 95 + const error: Ref<CombinedError | undefined> = shallowRef(); 96 + const operation: Ref<Operation<T, V> | undefined> = shallowRef(); 97 + const extensions: Ref<Record<string, any> | undefined> = shallowRef(); 98 + return { 99 + stale, 100 + fetching, 101 + error, 102 + operation, 103 + extensions, 104 + }; 105 + }; 106 + 107 + export function useClientState<T = any, V extends AnyVariables = AnyVariables>( 108 + args: UseQueryArgs<T, V> | UseSubscriptionArgs<T, V>, 109 + client: Ref<Client>, 110 + method: keyof Pick<Client, 'executeSubscription' | 'executeQuery'> 111 + ) { 112 + const source: Ref<OperationResultSource<OperationResult<T, V>> | undefined> = 113 + shallowRef(); 114 + 115 + const isPaused: Ref<boolean> = isRef(args.pause) 116 + ? args.pause 117 + : typeof args.pause === 'function' 118 + ? computed(args.pause) 119 + : ref(!!args.pause); 120 + 121 + const request = computed(() => createRequestWithArgs(args)); 122 + 123 + const requestOptions = computed(() => { 124 + return 'requestPolicy' in args 125 + ? { 126 + requestPolicy: unwrap(args.requestPolicy), 127 + ...unwrap(args.context), 128 + } 129 + : { 130 + ...unwrap(args.context), 131 + }; 132 + }); 133 + 134 + const pause = () => { 135 + if (!isReadonly(isPaused)) { 136 + isPaused.value = true; 137 + } 138 + }; 139 + 140 + const resume = () => { 141 + if (!isReadonly(isPaused)) { 142 + isPaused.value = false; 143 + } 144 + }; 145 + 146 + const executeRaw = (opts?: Partial<OperationContext>) => { 147 + return client.value[method]<T, V>(request.value, { 148 + ...requestOptions.value, 149 + ...opts, 150 + }); 151 + }; 152 + 153 + const execute = (opts?: Partial<OperationContext>) => { 154 + source.value = executeRaw(opts); 155 + }; 156 + 157 + // it's important to use `watchEffect()` here instead of `watch()` 158 + // because it listening for reactive variables inside `executeRaw()` function 159 + const teardown = watchEffect(() => { 160 + source.value = !isPaused.value ? executeRaw() : undefined; 161 + }); 162 + 163 + return { 164 + source, 165 + isPaused, 166 + pause, 167 + resume, 168 + execute, 169 + teardown, 170 + }; 171 + }