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