@gqty/react
Version:
The No-GraphQL Client for React
263 lines (260 loc) • 8.72 kB
JavaScript
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 };