Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 10 kB view raw
1import { 2 type AnyVariables, 3 type OperationContext, 4 type DocumentInput, 5 type OperationResult, 6 type RequestPolicy, 7 createRequest, 8} from '@urql/core'; 9import { 10 batch, 11 createComputed, 12 createMemo, 13 createResource, 14 createSignal, 15 onCleanup, 16} from 'solid-js'; 17import { createStore, produce, reconcile } from 'solid-js/store'; 18import { useClient } from './context'; 19import { type MaybeAccessor, asAccessor } from './utils'; 20import type { Source, Subscription } from 'wonka'; 21import { onEnd, pipe, subscribe } from 'wonka'; 22 23/** Triggers {@link createQuery} to execute a new GraphQL query operation. 24 * 25 * @remarks 26 * When called, {@link createQuery} will re-execute the GraphQL query operation 27 * it currently holds, even if {@link CreateQueryArgs.pause} is set to `true`. 28 * 29 * This is useful for executing a paused query or re-executing a query 30 * and get a new network result, by passing a new request policy. 31 * 32 * ```ts 33 * const [result, reExecuteQuery] = createQuery({ query }); 34 * 35 * const refresh = () => { 36 * // Re-execute the query with a network-only policy, skipping the cache 37 * reExecuteQuery({ requestPolicy: 'network-only' }); 38 * }; 39 * ``` 40 * 41 */ 42export type CreateQueryExecute = (opts?: Partial<OperationContext>) => void; 43 44/** State of the current query, your {@link createQuery} hook is executing. 45 * 46 * @remarks 47 * `CreateQueryState` is returned (in a tuple) by {@link createQuery} and 48 * gives you the updating {@link OperationResult} of GraphQL queries. 49 * 50 * Even when the query and variables passed to {@link createQuery} change, 51 * this state preserves the prior state and sets the `fetching` flag to 52 * `true`. 53 * This allows you to display the previous state, while implementing 54 * a separate loading indicator separately. 55 */ 56export type CreateQueryState< 57 Data = any, 58 Variables extends AnyVariables = AnyVariables, 59> = OperationResult<Data, Variables> & { 60 /** Indicates whether `createQuery` is waiting for a new result. 61 * 62 * @remarks 63 * When `createQuery` is passed a new query and/or variables, it will 64 * start executing the new query operation and `fetching` is set to 65 * `true` until a result arrives. 66 * 67 * Hint: This is subtly different than whether the query is actually 68 * fetching, and doesn’t indicate whether a query is being re-executed 69 * in the background. For this, see {@link CreateQueryState.stale}. 70 */ 71 fetching: boolean; 72}; 73 74/** 75 * Input arguments for the {@link createQuery} hook. 76 */ 77export type CreateQueryArgs< 78 Data = any, 79 Variables extends AnyVariables = AnyVariables, 80> = { 81 /** The GraphQL query that `createQuery` executes. */ 82 query: DocumentInput<Data, Variables>; 83 84 /** The variables for the GraphQL {@link CreateQueryArgs.query} that `createQuery` executes. */ 85 variables?: MaybeAccessor<Variables>; 86 87 /** Updates the {@link RequestPolicy} for the executed GraphQL query operation. 88 * 89 * @remarks 90 * `requestPolicy` modifies the {@link RequestPolicy} of the GraphQL query operation 91 * that `createQuery` executes, and indicates a caching strategy for cache exchanges. 92 * 93 * For example, when set to `'cache-and-network'`, {@link createQuery} will 94 * receive a cached result with `stale: true` and an API request will be 95 * sent in the background. 96 * 97 * @see {@link OperationContext.requestPolicy} for where this value is set. 98 */ 99 requestPolicy?: MaybeAccessor<RequestPolicy>; 100 101 /** Updates the {@link OperationContext} for the executed GraphQL query operation. 102 * 103 * @remarks 104 * `context` may be passed to {@link createQuery}, to update the {@link OperationContext} 105 * of a query operation. This may be used to update the `context` that exchanges 106 * will receive for a single hook. 107 * 108 * In order to re-execute query on context change pass {@link Accessor} instead 109 * of raw value. 110 */ 111 context?: MaybeAccessor<Partial<OperationContext>>; 112 113 /** Prevents {@link createQuery} from automatically executing GraphQL query operations. 114 * 115 * @remarks 116 * `pause` may be set to `true` to stop {@link createQuery} from executing 117 * automatically. The hook will stop receiving updates from the {@link Client} 118 * and won’t execute the query operation, until either it’s set to `false` 119 * or the {@link CreateQueryExecute} function is called. 120 */ 121 pause?: MaybeAccessor<boolean>; 122}; 123 124/** Result tuple returned by the {@link createQuery} hook. 125 * 126 * @remarks 127 * the first element is the {@link createQuery}’s result and state, 128 * a {@link CreateQueryState} object, 129 * and the second is used to imperatively re-execute the query 130 * via a {@link CreateQueryExecute} function. 131 */ 132export type CreateQueryResult< 133 Data = any, 134 Variables extends AnyVariables = AnyVariables, 135> = [CreateQueryState<Data, Variables>, CreateQueryExecute]; 136 137/** Hook to run a GraphQL query and get updated GraphQL results. 138 * 139 * @param args - a {@link CreateQueryArgs} object, to pass a `query`, `variables`, and options. 140 * @returns a {@link CreateQueryResult} tuple of a {@link CreateQueryState} result, and re-execute function. 141 * 142 * @remarks 143 * `createQuery` allows GraphQL queries to be defined and executed. 144 * Given {@link CreateQueryArgs.query}, it executes the GraphQL query with the 145 * context’s {@link Client}. 146 * 147 * The returned result updates when the `Client` has new results 148 * for the query, and changes when your input `args` change. 149 * 150 * Additionally, if the `suspense` option is enabled on the `Client`, 151 * the `createQuery` hook will suspend instead of indicating that it’s 152 * waiting for a result via {@link CreateQueryState.fetching}. 153 * 154 * @example 155 * ```tsx 156 * import { gql, createQuery } from '@urql/solid'; 157 * 158 * const TodosQuery = gql` 159 * query { todos { id, title } } 160 * `; 161 * 162 * const Todos = () => { 163 * const [result, reExecuteQuery] = createQuery({ 164 * query: TodosQuery, 165 * }); 166 * // ... 167 * }; 168 * ``` 169 */ 170export const createQuery = < 171 Data = any, 172 Variables extends AnyVariables = AnyVariables, 173>( 174 args: CreateQueryArgs<Data, Variables> 175): CreateQueryResult<Data, Variables> => { 176 const client = useClient(); 177 const getContext = asAccessor(args.context); 178 const getPause = asAccessor(args.pause); 179 const getRequestPolicy = asAccessor(args.requestPolicy); 180 const getVariables = asAccessor(args.variables); 181 182 const [source, setSource] = createSignal< 183 Source<OperationResult<Data, Variables>> | undefined 184 >(undefined, { equals: false }); 185 186 // Combine suspense param coming from context and client with context being priority 187 const isSuspense = createMemo(() => { 188 const ctx = getContext(); 189 if (ctx !== undefined && ctx.suspense !== undefined) { 190 return ctx.suspense; 191 } 192 193 return client.suspense; 194 }); 195 196 const request = createRequest(args.query, getVariables() as any); 197 const context: Partial<OperationContext> = { 198 requestPolicy: getRequestPolicy(), 199 ...getContext(), 200 }; 201 const operation = client.createRequestOperation('query', request, context); 202 const initialResult: CreateQueryState<Data, Variables> = { 203 operation: operation, 204 fetching: false, 205 data: undefined, 206 error: undefined, 207 extensions: undefined, 208 hasNext: false, 209 stale: false, 210 }; 211 212 const [result, setResult] = 213 createStore<CreateQueryState<Data, Variables>>(initialResult); 214 215 createComputed(() => { 216 if (getPause() === true) { 217 setSource(undefined); 218 return; 219 } 220 221 const request = createRequest(args.query, getVariables() as any); 222 const context: Partial<OperationContext> = { 223 requestPolicy: getRequestPolicy(), 224 ...getContext(), 225 }; 226 227 setSource(() => client.executeQuery(request, context)); 228 }); 229 230 createComputed(() => { 231 const s = source(); 232 if (s === undefined) { 233 setResult( 234 produce(draft => { 235 draft.fetching = false; 236 draft.stale = false; 237 draft.hasNext = false; 238 }) 239 ); 240 241 return; 242 } 243 244 setResult( 245 produce(draft => { 246 draft.fetching = true; 247 draft.stale = false; 248 draft.hasNext = false; 249 }) 250 ); 251 252 onCleanup( 253 pipe( 254 s, 255 onEnd(() => { 256 setResult( 257 produce(draft => { 258 draft.fetching = false; 259 draft.stale = false; 260 draft.hasNext = false; 261 }) 262 ); 263 }), 264 subscribe(res => { 265 batch(() => { 266 setResult('data', reconcile(res.data)); 267 setResult( 268 produce(draft => { 269 draft.stale = !!res.stale; 270 draft.fetching = false; 271 draft.error = res.error; 272 draft.operation = res.operation; 273 draft.extensions = res.extensions; 274 draft.hasNext = res.hasNext; 275 }) 276 ); 277 }); 278 }) 279 ).unsubscribe 280 ); 281 }); 282 283 const [dataResource, { refetch }] = createResource< 284 CreateQueryState<Data, Variables>, 285 Source<OperationResult<Data, Variables>> | undefined 286 >(source, source => { 287 let sub: Subscription | void; 288 if (source === undefined) { 289 return Promise.resolve(result); 290 } 291 292 return new Promise<CreateQueryState<Data, Variables>>(resolve => { 293 let hasResult = false; 294 sub = pipe( 295 source, 296 subscribe(() => { 297 if (!result.fetching && !result.stale) { 298 if (sub) sub.unsubscribe(); 299 hasResult = true; 300 resolve(result); 301 } 302 }) 303 ); 304 if (hasResult) { 305 sub.unsubscribe(); 306 } 307 }); 308 }); 309 310 const executeQuery: CreateQueryExecute = opts => { 311 const request = createRequest(args.query, getVariables() as any); 312 const context: Partial<OperationContext> = { 313 requestPolicy: getRequestPolicy(), 314 ...getContext(), 315 ...opts, 316 }; 317 318 setSource(() => client.executeQuery(request, context)); 319 if (isSuspense()) { 320 refetch(); 321 } 322 }; 323 324 const handler = { 325 get( 326 target: CreateQueryState<Data, Variables>, 327 prop: keyof CreateQueryState<Data, Variables> 328 ): any { 329 if (isSuspense() && prop === 'data') { 330 const resource = dataResource(); 331 if (resource === undefined) return undefined; 332 } 333 334 return Reflect.get(target, prop); 335 }, 336 }; 337 338 const proxy = new Proxy(result, handler); 339 return [proxy, executeQuery]; 340};