@duongtrungnguyen/next-helper
Version:
Helper library for Next.js 15
464 lines • 14.3 kB
JavaScript
"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import { queryClient } from "./query-client";
import { queryCache } from "./cache";
const retryFn = async (fn, retries, retryDelay) => {
let attempts = 0;
const execute = async () => {
try {
return await fn();
} catch (error) {
const maxRetries = typeof retries === "number" ? retries : retries === true ? 3 : 0;
if (attempts >= maxRetries) {
throw error;
}
attempts++;
const delay = typeof retryDelay === "function" ? retryDelay(attempts) : typeof retryDelay === "number" ? retryDelay : Math.min(1e3 * 2 ** attempts, 3e4);
await new Promise((resolve) => setTimeout(resolve, delay));
return execute();
}
};
return execute();
};
function useQuery(options) {
const {
queryKey,
queryFn,
enabled = true,
staleTime = 5 * 60 * 1e3,
// 5 minutes
cacheTime = 30 * 60 * 1e3,
// 30 minutes
retry = 3,
retryDelay = (attempt) => Math.min(1e3 * 2 ** attempt, 3e4),
onSuccess,
onError,
onSettled,
initialData,
refetchInterval = false,
refetchOnWindowFocus = true,
refetchOnMount = true,
refetchOnReconnect = true,
select = (data2) => data2
} = options;
const [data, setData] = useState(() => {
const cachedData = queryCache.get(queryKey);
if (cachedData !== void 0) {
return select(cachedData);
}
if (initialData) {
const initialValue = typeof initialData === "function" ? initialData() : initialData;
if (initialValue !== void 0) {
queryCache.set(queryKey, initialValue, staleTime, cacheTime);
return select(initialValue);
}
}
return void 0;
});
const [error, setError] = useState(null);
const [status, setStatus] = useState(
data !== void 0 ? "success" : "idle"
);
const [isFetching, setIsFetching] = useState(false);
const refetchIntervalRef = useRef(null);
const abortControllerRef = useRef(null);
const mountedRef = useRef(true);
const fetchData = useCallback(async () => {
if (!enabled) {
return data;
}
setIsFetching(true);
queryClient.setFetching(queryKey, true);
if (status === "idle") {
setStatus("loading");
}
abortControllerRef.current = new AbortController();
try {
const result = await retryFn(
() => {
var _a;
return queryFn({}, {
queryKey,
signal: (_a = abortControllerRef.current) == null ? void 0 : _a.signal
});
},
retry,
retryDelay
);
if (!mountedRef.current) return result;
queryCache.set(queryKey, result, staleTime, cacheTime);
const selectedData = select(result);
setData(selectedData);
setError(null);
setStatus("success");
onSuccess == null ? void 0 : onSuccess(selectedData);
onSettled == null ? void 0 : onSettled(selectedData, null);
return selectedData;
} catch (err) {
if (!mountedRef.current) throw err;
const error2 = err instanceof Error ? err : new Error(String(err));
setError(error2);
setStatus("error");
onError == null ? void 0 : onError(error2);
onSettled == null ? void 0 : onSettled(void 0, error2);
throw error2;
} finally {
if (mountedRef.current) {
setIsFetching(false);
queryClient.setFetching(queryKey, false);
}
}
}, [
queryKey,
queryFn,
enabled,
staleTime,
cacheTime,
retry,
retryDelay,
onSuccess,
onError,
onSettled,
select,
status,
data
]);
useEffect(() => {
const unsubscribe = queryCache.subscribe(queryKey, () => {
const cachedData = queryCache.get(queryKey);
if (cachedData !== void 0) {
setData(select(cachedData));
setError(null);
setStatus("success");
}
});
return unsubscribe;
}, [queryKey, select]);
useEffect(() => {
mountedRef.current = true;
if (enabled && (refetchOnMount || queryCache.isStale(queryKey))) {
fetchData().catch(() => {
});
}
return () => {
mountedRef.current = false;
if (refetchIntervalRef.current) {
clearInterval(refetchIntervalRef.current);
}
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, [enabled, fetchData, queryKey, refetchOnMount]);
useEffect(() => {
if (!refetchInterval || !enabled) {
return;
}
refetchIntervalRef.current = setInterval(() => {
if (document.visibilityState === "visible") {
fetchData().catch(() => {
});
}
}, refetchInterval);
return () => {
if (refetchIntervalRef.current) {
clearInterval(refetchIntervalRef.current);
}
};
}, [refetchInterval, fetchData, enabled]);
useEffect(() => {
if (!refetchOnWindowFocus || !enabled) {
return;
}
const handleFocus = () => {
if (queryCache.isStale(queryKey)) {
fetchData().catch(() => {
});
}
};
window.addEventListener("focus", handleFocus);
return () => {
window.removeEventListener("focus", handleFocus);
};
}, [refetchOnWindowFocus, fetchData, queryKey, enabled]);
useEffect(() => {
if (!refetchOnReconnect || !enabled) {
return;
}
const handleOnline = () => {
fetchData().catch(() => {
});
};
window.addEventListener("online", handleOnline);
return () => {
window.removeEventListener("online", handleOnline);
};
}, [refetchOnReconnect, fetchData, enabled]);
return {
data,
error,
isLoading: status === "loading",
isError: status === "error",
isSuccess: status === "success",
isFetching,
refetch: fetchData,
status
};
}
function useMutation(options) {
const {
mutationFn,
onMutate,
onSuccess,
onError,
onSettled,
retry = 0,
retryDelay = (attempt) => Math.min(1e3 * 2 ** attempt, 3e4)
} = options;
const [data, setData] = useState(void 0);
const [error, setError] = useState(null);
const [status, setStatus] = useState("idle");
const reset = useCallback(() => {
setData(void 0);
setError(null);
setStatus("idle");
}, []);
const mutate = useCallback(
async (variables) => {
setStatus("loading");
setData(void 0);
setError(null);
let context;
try {
if (onMutate) {
context = await onMutate(variables);
}
const result = await retryFn(() => mutationFn(variables), retry, retryDelay);
setData(result);
setStatus("success");
if (onSuccess) {
await onSuccess(result, variables, context);
}
if (onSettled) {
await onSettled(result, null, variables, context);
}
return result;
} catch (err) {
const error2 = err instanceof Error ? err : new Error(String(err));
setError(error2);
setStatus("error");
if (onError) {
await onError(error2, variables, context);
}
if (onSettled) {
await onSettled(void 0, error2, variables, context);
}
throw error2;
}
},
[mutationFn, onMutate, onSuccess, onError, onSettled, retry, retryDelay]
);
return {
data,
error,
isLoading: status === "loading",
isError: status === "error",
isSuccess: status === "success",
isIdle: status === "idle",
reset,
mutate,
mutateAsync: mutate,
status
};
}
function useInfiniteQuery(options) {
const { queryKey, queryFn, getNextPageParam, getPreviousPageParam, initialPageParam = 0, ...queryOptions } = options;
const [pages, setPages] = useState([]);
const [pageParams, setPageParams] = useState([initialPageParam]);
const [isFetchingNextPage, setIsFetchingNextPage] = useState(false);
const [isFetchingPreviousPage, setIsFetchingPreviousPage] = useState(false);
const getPageQueryKey = (pageParam) => [...queryKey, { page: pageParam }];
const queryResult = useQuery({
...queryOptions,
queryKey: getPageQueryKey(initialPageParam),
queryFn: async (params, context) => {
const result = await queryFn({ ...params, pageParam: initialPageParam }, context);
return result;
},
onSuccess: (data) => {
var _a;
setPages([data]);
(_a = queryOptions.onSuccess) == null ? void 0 : _a.call(queryOptions, data);
}
});
const { refetch, ...rest } = queryResult;
const hasNextPage = pages.length > 0 && getNextPageParam(pages[pages.length - 1], pages) !== void 0;
const hasPreviousPage = pages.length > 0 && (getPreviousPageParam == null ? void 0 : getPreviousPageParam(pages[0], pages)) !== void 0;
const fetchNextPage = useCallback(async () => {
if (!hasNextPage || isFetchingNextPage) {
return pages;
}
setIsFetchingNextPage(true);
try {
const lastPage = pages[pages.length - 1];
const nextPageParam = getNextPageParam(lastPage, pages);
if (nextPageParam === void 0) {
return pages;
}
const pageQueryKey = getPageQueryKey(nextPageParam);
const cachedData = queryCache.get(pageQueryKey);
if (cachedData) {
setPages([...pages, cachedData]);
setPageParams([...pageParams, nextPageParam]);
return [...pages, cachedData];
}
const newPageData = await queryFn({ pageParam: nextPageParam }, {
queryKey: pageQueryKey
});
queryCache.set(pageQueryKey, newPageData);
const newPages = [...pages, newPageData];
setPages(newPages);
setPageParams([...pageParams, nextPageParam]);
return newPages;
} finally {
setIsFetchingNextPage(false);
}
}, [hasNextPage, isFetchingNextPage, pages, getNextPageParam, pageParams, queryFn, getPageQueryKey]);
const fetchPreviousPage = useCallback(async () => {
if (!hasPreviousPage || isFetchingPreviousPage || !getPreviousPageParam) {
return pages;
}
setIsFetchingPreviousPage(true);
try {
const firstPage = pages[0];
const previousPageParam = getPreviousPageParam(firstPage, pages);
if (previousPageParam === void 0) {
return pages;
}
const pageQueryKey = getPageQueryKey(previousPageParam);
const cachedData = queryCache.get(pageQueryKey);
if (cachedData) {
setPages([cachedData, ...pages]);
setPageParams([previousPageParam, ...pageParams]);
return [cachedData, ...pages];
}
const newPageData = await queryFn({ pageParam: previousPageParam }, {
queryKey: pageQueryKey
});
queryCache.set(pageQueryKey, newPageData);
const newPages = [newPageData, ...pages];
setPages(newPages);
setPageParams([previousPageParam, ...pageParams]);
return newPages;
} finally {
setIsFetchingPreviousPage(false);
}
}, [hasPreviousPage, isFetchingPreviousPage, pages, getPreviousPageParam, pageParams, queryFn, getPageQueryKey]);
const refetchAll = useCallback(async () => {
const newPages = [];
for (let i = 0; i < pageParams.length; i++) {
const pageParam = pageParams[i];
const pageQueryKey = getPageQueryKey(pageParam);
queryCache.invalidate(pageQueryKey);
const newPageData = await queryFn({ pageParam }, {
queryKey: pageQueryKey
});
queryCache.set(pageQueryKey, newPageData);
newPages.push(newPageData);
}
setPages(newPages);
return newPages;
}, [pageParams, queryFn]);
return {
...rest,
data: pages.length > 0 ? pages : void 0,
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
isFetchingNextPage,
isFetchingPreviousPage,
refetch: refetchAll
};
}
function usePaginatedQuery(options) {
var _a, _b, _c;
const {
queryKey,
queryFn,
pageSize = 10,
pageIndex: initialPageIndex = 0,
keepPreviousData = true,
...queryOptions
} = options;
const [pageIndex, setPageIndex] = useState(initialPageIndex);
const paginatedQueryKey = [...queryKey, { pageIndex, pageSize }];
const previousDataRef = useRef(null);
const queryResult = useQuery({
...queryOptions,
queryKey: paginatedQueryKey,
queryFn: async (params, context) => {
const combinedParams = {
...params,
pagination: {
pageIndex,
pageSize
},
queryKey: context.queryKey,
signal: context.signal
};
return queryFn(combinedParams);
}
});
useEffect(() => {
if (queryResult.data && !queryResult.isLoading) {
previousDataRef.current = queryResult.data;
}
}, [queryResult.data, queryResult.isLoading]);
const data = queryResult.isLoading && keepPreviousData && previousDataRef.current ? previousDataRef.current.data : (_a = queryResult.data) == null ? void 0 : _a.data;
const totalCount = queryResult.isLoading && keepPreviousData && previousDataRef.current ? previousDataRef.current.totalCount : ((_b = queryResult.data) == null ? void 0 : _b.totalCount) || 0;
const pageCount = queryResult.isLoading && keepPreviousData && previousDataRef.current ? previousDataRef.current.pageCount : ((_c = queryResult.data) == null ? void 0 : _c.pageCount) || 0;
const canPreviousPage = pageIndex > 0;
const canNextPage = pageIndex < pageCount - 1;
const previousPage = useCallback(() => {
setPageIndex((old) => Math.max(0, old - 1));
}, []);
const nextPage = useCallback(() => {
setPageIndex((old) => Math.min(pageCount - 1, old + 1));
}, [pageCount]);
return {
...queryResult,
data,
totalCount,
pageCount,
pageIndex,
pageSize,
setPageIndex,
previousPage,
nextPage,
canPreviousPage,
canNextPage
};
}
function useQueryClient() {
return queryClient;
}
function useIsFetching() {
const [count, setCount] = useState(queryClient.isFetching());
useEffect(() => {
const interval = setInterval(() => {
setCount(queryClient.isFetching());
}, 100);
return () => clearInterval(interval);
}, []);
return count;
}
export {
useInfiniteQuery,
useIsFetching,
useMutation,
usePaginatedQuery,
useQuery,
useQueryClient
};
//# sourceMappingURL=hooks.js.map