UNPKG

@gqty/react

Version:

The No-GraphQL Client for React

263 lines (260 loc) • 8.72 kB
import { useRerender, useIntervalEffect, usePrevious, useUpdateEffect } from '@react-hookz/web'; import { prepass, GQtyError } from 'gqty'; import { MultiDict } from 'multidict'; import { useRef, useMemo, useState, useEffect, useCallback } from 'react'; import ModifiedSet from '../ModifiedSet.mjs'; import { translateFetchPolicy } from '../common.mjs'; import { useOnlineEffect } from '../useOnlineEffect.mjs'; import { useRenderSession } from '../useRenderSession.mjs'; import { useWindowFocusEffect } from '../useWindowFocusEffect.mjs'; const createUseQuery = (client, { defaults: { initialLoadingState: defaultInitialLoadingState, suspense: defaultSuspense, staleWhileRevalidate: defaultStaleWhileRevalidate, retry: defaultRetry } }) => { const cofetchingResolvers = new MultiDict(); const resolverStack = new ModifiedSet(); return ({ extensions, fetchInBackground = false, fetchPolicy, cachePolicy = translateFetchPolicy(fetchPolicy != null ? fetchPolicy : "cache-first"), initialLoadingState = defaultInitialLoadingState, suspense = defaultSuspense, notifyOnNetworkStatusChange = !suspense, onError, operationName, prepare, refetchInterval, refetchOnReconnect = true, refetchOnRender = true, refetchOnWindowVisible = true, retry = defaultRetry, retryPolicy = retry, staleWhileRevalidate = defaultStaleWhileRevalidate } = {}) => { const initialStateRef = useRef(initialLoadingState); const render = useRerender(); const renderSession = useRenderSession(); renderSession.set("isRendering", true); const resolver = useMemo(() => { const resolver2 = client.createResolver({ cachePolicy, extensions, operationName, retryPolicy, onSelect() { const currentResolver = resolverStack.lastAdded; if (currentResolver && currentResolver !== resolver2) { cofetchingResolvers.set(currentResolver, resolver2); } resolverStack.add(resolver2); if (!renderSession.get("isRendering")) { Promise.resolve().then( () => refetch({ skipPrepass: true, skipOnError: true }) ); } else if (!renderSession.get("postFetch")) { if (cachePolicy === "reload" || cachePolicy === "no-cache" || cachePolicy === "no-store") { resolver2.context.shouldFetch = true; } if (!renderSession.get("postFetchSelectionCleared")) { renderSession.set("postFetchSelectionCleared", true); selections.clear(); } } } }); return resolver2; }, [cachePolicy, operationName, retryPolicy]); const { accessor: { query }, accessor, context, resolve, selections } = resolver; const [state, setState] = useState({}); if (prepare) { context.shouldFetch = false; prepare({ prepass, query }); if (context.shouldFetch && suspense) { throw client.resolve(({ query: query2 }) => prepare({ prepass, query: query2 }), { cachePolicy, operationName, retryPolicy }); } } if (suspense) { if (state.error) { throw state.error; } if (state.promise && (!context.hasCacheHit || notifyOnNetworkStatusChange)) { throw state.promise; } } useEffect( () => context.cache.subscribe( [...selections].map((s) => s.cacheKeys.join(".")), render ), [render, selections.size] ); const refetch = useCallback( async (options) => { var _a, _b, _c; if (state.promise !== void 0) { return; } if (state.error && ((_a = options == null ? void 0 : options.skipOnError) != null ? _a : !suspense)) { return; } if (!fetchInBackground && ((_b = globalThis.document) == null ? void 0 : _b.visibilityState) === "hidden") { return; } if ((options == null ? void 0 : options.ignoreCache) === true) { context.shouldFetch = true; } else { if (!(options == null ? void 0 : options.skipPrepass) && isFinite(client.cache.maxAge)) { prepass(accessor, selections); } if (renderSession.get("postFetch") && !context.hasCacheMiss) { context.shouldFetch = false; } } if (!context.shouldFetch) { return; } initialStateRef.current = false; try { if (!client.cache.normalizationOptions) { const seen = /* @__PURE__ */ new Set(); for (const { cacheKeys: [type, field] = [] } of selections) { if (type !== "query") { continue; } if (seen.has(field)) { continue; } else { seen.add(field); } resolversLoop: for (const stackResolver of resolverStack) { if (stackResolver === resolver) { continue; } for (const { cacheKeys: [objType, objField] } of stackResolver.selections) { if (type === objType && field === objField) { cofetchingResolvers.set(resolver, stackResolver); continue resolversLoop; } } } } } const pendingPromises = [...(_c = cofetchingResolvers.get(resolver)) != null ? _c : []].map(({ context: context2, resolve: resolve2 }) => { context2.shouldFetch = true; const ret = resolve2(); context2.shouldFetch = false; return ret; }).concat(resolve()); context.shouldFetch = false; const promise = Promise.all(pendingPromises); state.promise = promise; if (!context.hasCacheHit || notifyOnNetworkStatusChange) { setState({ promise }); } await promise; } catch (e) { const error = GQtyError.create(e); onError == null ? void 0 : onError(error); setState({ error }); } finally { context.reset(); state.promise = void 0; resolverStack.clear(); cofetchingResolvers.delete(resolver); renderSession.set("postFetch", true); setState(({ error }) => ({ error, promise: void 0 })); } }, [ cachePolicy, context.shouldFetch, fetchInBackground, operationName, selections ] ); useEffect(() => { if (initialLoadingState && selections.size === 0) { if (process.env.NODE_ENV !== "production") { console.warn( "[GQty] No selections found, this is rarely expected with initialLoadingState." ); } } }, []); useEffect(() => { if (!refetchOnRender) { return; } refetch({ skipPrepass: true }); }); useIntervalEffect(() => { refetch(); }, refetchInterval); useOnlineEffect(() => { if (!refetchOnReconnect) { return; } refetch(); }, [refetchOnReconnect]); useWindowFocusEffect(() => { if (!refetchOnWindowVisible) { return; } refetch(); }); const swrDiff = usePrevious(staleWhileRevalidate); useUpdateEffect(() => { if (!staleWhileRevalidate || Object.is(staleWhileRevalidate, swrDiff)) { return; } refetch({ ignoreCache: true }); }, [refetch, swrDiff]); return useMemo(() => { return new Proxy( Object.freeze({ $refetch: (ignoreCache = true) => refetch({ ignoreCache, skipOnError: false }), $state: Object.freeze({ isLoading: state.promise !== void 0 || initialStateRef.current, error: state.error }) }), { get: (target, key, proxy) => { var _a; return (_a = Reflect.get(target, key, proxy)) != null ? _a : Reflect.get( prepare && cachePolicy !== "no-store" ? ( // Using global schema accessor prevents the second pass fetch // essentially let `prepare` decides what data to fetch, data // placeholder will always render in case of a cache miss. client.schema.query ) : query, key ); } } ); }, [query, refetch, state.error, state.promise]); }; }; export { createUseQuery };