Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 9.9 kB view raw
1/* eslint-disable react-hooks/rules-of-hooks */ 2 3import { pipe, subscribe, onEnd } from 'wonka'; 4 5import type { Ref, WatchStopHandle } from 'vue'; 6import { shallowRef, isRef, watchEffect } from 'vue'; 7 8import type { 9 Client, 10 GraphQLRequestParams, 11 AnyVariables, 12 CombinedError, 13 OperationContext, 14 Operation, 15} from '@urql/core'; 16 17import { useClient } from './useClient'; 18 19import type { MaybeRefOrGetter, MaybeRefOrGetterObj } from './utils'; 20import { useRequestState, useClientState } from './utils'; 21 22/** Input arguments for the {@link useSubscription} function. 23 * 24 * @param query - The GraphQL subscription document that `useSubscription` executes. 25 * @param variables - The variables for the GraphQL subscription that `useSubscription` executes. 26 */ 27export type UseSubscriptionArgs< 28 Data = any, 29 Variables extends AnyVariables = AnyVariables, 30> = { 31 /** Prevents {@link useSubscription} from automatically executing GraphQL subscription operations. 32 * 33 * @remarks 34 * `pause` may be set to `true` to stop {@link useSubscription} from starting 35 * its subscription automatically. This will pause the subscription until 36 * {@link UseSubscriptionResponse.resume} is called, or, if `pause` is a reactive 37 * ref of a boolean, until this ref changes to `true`. 38 */ 39 pause?: MaybeRefOrGetter<boolean>; 40 /** Updates the {@link OperationContext} for the executed GraphQL subscription operation. 41 * 42 * @remarks 43 * `context` may be passed to {@link useSubscription}, to update the {@link OperationContext} 44 * of a subscription operation. This may be used to update the `context` that exchanges 45 * will receive for a single hook. 46 * 47 * @example 48 * ```ts 49 * const result = useQuery({ 50 * query, 51 * context: { 52 * additionalTypenames: ['Item'], 53 * }, 54 * }); 55 * ``` 56 */ 57 context?: MaybeRefOrGetter<Partial<OperationContext>>; 58} & MaybeRefOrGetterObj<GraphQLRequestParams<Data, Variables>>; 59 60/** Combines previous data with an incoming subscription result’s data. 61 * 62 * @remarks 63 * A `SubscriptionHandler` may be passed to {@link useSubscription} to 64 * aggregate subscription results into a combined {@link UseSubscriptionResponse.data} 65 * value. 66 * 67 * This is useful when a subscription event delivers a single item, while 68 * you’d like to display a list of events. 69 * 70 * @example 71 * ```ts 72 * const NotificationsSubscription = gql` 73 * subscription { newNotification { id, text } } 74 * `; 75 * 76 * const combineNotifications = (notifications = [], data) => { 77 * return [...notifications, data.newNotification]; 78 * }; 79 * 80 * const result = useSubscription( 81 * { query: NotificationsSubscription }, 82 * combineNotifications, 83 * ); 84 * ``` 85 */ 86export type SubscriptionHandler<T, R> = (prev: R | undefined, data: T) => R; 87 88/** A {@link SubscriptionHandler} or a reactive ref of one. */ 89export type SubscriptionHandlerArg<T, R> = 90 | Ref<SubscriptionHandler<T, R>> 91 | SubscriptionHandler<T, R>; 92 93/** State of the current query, your {@link useSubscription} function is executing. 94 * 95 * @remarks 96 * `UseSubscriptionResponse` is returned by {@link useSubscription} and 97 * gives you the updating {@link OperationResult} of GraphQL subscriptions. 98 * 99 * Each value that is part of the result is wrapped in a reactive ref 100 * and updates as results come in. 101 * 102 * Hint: Even when the query and variables update, the prior state of 103 * the last result is preserved. 104 */ 105export interface UseSubscriptionResponse< 106 T = any, 107 R = T, 108 V extends AnyVariables = AnyVariables, 109> { 110 /** Indicates whether `useSubscription`’s subscription is active. 111 * 112 * @remarks 113 * When `useSubscription` starts a subscription, the `fetching` flag 114 * is set to `true` and will remain `true` until the subscription 115 * completes on the API, or `useSubscription` is paused. 116 */ 117 fetching: Ref<boolean>; 118 /** Indicates that the subscription result is not fresh. 119 * 120 * @remarks 121 * This is mostly unused for subscriptions and will rarely affect you, and 122 * is more relevant for queries. 123 * 124 * @see {@link OperationResult.stale} for the source of this value. 125 */ 126 stale: Ref<boolean>; 127 /** Reactive {@link OperationResult.data} for the executed subscription, or data returned by the handler. 128 * 129 * @remarks 130 * `data` will be set to the last {@link OperationResult.data} value 131 * received for the subscription. 132 * 133 * It will instead be set to the values that {@link SubscriptionHandler} 134 * returned, if a handler has been passed to {@link useSubscription}. 135 */ 136 data: Ref<R | undefined>; 137 /** Reactive {@link OperationResult.error} for the executed subscription. */ 138 error: Ref<CombinedError | undefined>; 139 /** Reactive {@link OperationResult.extensions} for the executed mutation. */ 140 extensions: Ref<Record<string, any> | undefined>; 141 /** Reactive {@link Operation} that the current state is for. 142 * 143 * @remarks 144 * This is the subscription {@link Operation} that is currently active. 145 * When {@link UseSubscriptionResponse.fetching} is `true`, this is the 146 * last `Operation` that the current state was for. 147 */ 148 operation: Ref<Operation<T, V> | undefined>; 149 /** Indicates whether {@link useSubscription} is currently paused. 150 * 151 * @remarks 152 * When `useSubscription` has been paused, it will stop receiving updates 153 * from the {@link Client} and won’t execute the subscription, until 154 * {@link UseSubscriptionArgs.pause} becomes true or 155 * {@link UseSubscriptionResponse.resume} is called. 156 */ 157 isPaused: Ref<boolean>; 158 /** Resumes {@link useSubscription} if it’s currently paused. 159 * 160 * @remarks 161 * Resumes or starts {@link useSubscription}’s subscription, if it’s currently paused. 162 */ 163 resume(): void; 164 /** Pauses {@link useSubscription} to stop the subscription. 165 * 166 * @remarks 167 * Pauses {@link useSubscription}’s subscription, which stops it 168 * from receiving updates from the {@link Client} and to stop executing 169 * the subscription operation. 170 */ 171 pause(): void; 172 /** Triggers {@link useQuery} to reexecute a GraphQL subscription operation. 173 * 174 * @param opts - optionally, context options that will be merged with 175 * {@link UseQueryArgs.context} and the `Client`’s options. 176 * 177 * @remarks 178 * When called, {@link useSubscription} will re-execute the GraphQL subscription 179 * operation it currently holds, unless it’s currently paused. 180 */ 181 executeSubscription(opts?: Partial<OperationContext>): void; 182} 183 184/** Function to run a GraphQL subscription and get reactive GraphQL results. 185 * 186 * @param args - a {@link UseSubscriptionArgs} object, to pass a `query`, `variables`, and options. 187 * @param handler - optionally, a {@link SubscriptionHandler} function to combine multiple subscription results. 188 * @returns a {@link UseSubscriptionResponse} object. 189 * 190 * @remarks 191 * `useSubscription` allows GraphQL subscriptions to be defined and executed inside 192 * Vue `setup` functions. 193 * Given {@link UseSubscriptionArgs.query}, it executes the GraphQL subscription with the 194 * provided {@link Client}. 195 * 196 * The returned result updates when the `Client` has new results 197 * for the subscription, and `data` is updated with the result’s data 198 * or with the `data` that a `handler` returns. 199 * 200 * @example 201 * ```ts 202 * import { gql, useSubscription } from '@urql/vue'; 203 * 204 * const NotificationsSubscription = gql` 205 * subscription { newNotification { id, text } } 206 * `; 207 * 208 * export default { 209 * setup() { 210 * const result = useSubscription( 211 * { query: NotificationsSubscription }, 212 * function combineNotifications(notifications = [], data) { 213 * return [...notifications, data.newNotification]; 214 * }, 215 * ); 216 * // ... 217 * }, 218 * }; 219 * ``` 220 */ 221export function useSubscription< 222 T = any, 223 R = T, 224 V extends AnyVariables = AnyVariables, 225>( 226 args: UseSubscriptionArgs<T, V>, 227 handler?: SubscriptionHandlerArg<T, R> 228): UseSubscriptionResponse<T, R, V> { 229 return callUseSubscription(args, handler); 230} 231 232export function callUseSubscription< 233 T = any, 234 R = T, 235 V extends AnyVariables = AnyVariables, 236>( 237 args: UseSubscriptionArgs<T, V>, 238 handler?: SubscriptionHandlerArg<T, R>, 239 client: Ref<Client> = useClient(), 240 stops?: WatchStopHandle[] 241): UseSubscriptionResponse<T, R, V> { 242 const data: Ref<R | undefined> = shallowRef(); 243 244 const { fetching, operation, extensions, stale, error } = useRequestState< 245 T, 246 V 247 >(); 248 249 const { isPaused, source, pause, resume, execute, teardown } = useClientState< 250 T, 251 V 252 >(args, client, 'executeSubscription'); 253 254 const teardownSubscription = watchEffect(onInvalidate => { 255 if (source.value) { 256 fetching.value = true; 257 258 onInvalidate( 259 pipe( 260 source.value, 261 onEnd(() => { 262 fetching.value = false; 263 }), 264 subscribe(result => { 265 fetching.value = true; 266 error.value = result.error; 267 extensions.value = result.extensions; 268 stale.value = !!result.stale; 269 operation.value = result.operation; 270 271 if (result.data != null && handler) { 272 const cb = isRef(handler) ? handler.value : handler; 273 if (typeof cb === 'function') { 274 data.value = cb(data.value, result.data); 275 return; 276 } 277 } 278 data.value = result.data as R; 279 }) 280 ).unsubscribe 281 ); 282 } else { 283 fetching.value = false; 284 } 285 }); 286 287 stops && stops.push(teardown, teardownSubscription); 288 289 const state: UseSubscriptionResponse<T, R, V> = { 290 data, 291 stale, 292 error, 293 operation, 294 extensions, 295 fetching, 296 isPaused, 297 pause, 298 resume, 299 executeSubscription( 300 opts?: Partial<OperationContext> 301 ): UseSubscriptionResponse<T, R, V> { 302 execute(opts); 303 return state; 304 }, 305 }; 306 307 return state; 308}