next-unified-query
Version:
React hooks and components for next-unified-query-core
346 lines (344 loc) • 11 kB
JavaScript
"use client";
import React, { createContext, useRef, useContext, useCallback, useSyncExternalStore, useEffect, useReducer } from 'react';
import { isObject, has, isFunction, merge, isArray } from 'es-toolkit/compat';
import { getQueryClient, validateQueryConfig, QueryObserver, validateMutationConfig, z, FetchError } from 'next-unified-query-core';
// src/hooks/use-query.ts
var QueryClientContext = createContext(null);
function HydrationBoundary({
state,
children
}) {
const client = useQueryClient();
const hydratedRef = useRef(false);
if (state && !hydratedRef.current) {
client.hydrate(state);
hydratedRef.current = true;
}
return /* @__PURE__ */ React.createElement(React.Fragment, null, children);
}
function QueryClientProvider({ client, options, children }) {
const queryClient = client || getQueryClient(options);
return /* @__PURE__ */ React.createElement(QueryClientContext.Provider, { value: queryClient }, children);
}
function useQueryClient() {
const ctx = useContext(QueryClientContext);
if (!ctx) throw new Error("You must wrap your component tree with <QueryClientProvider>.");
return ctx;
}
function useQuery(arg1, arg2) {
if (isObject(arg1) && has(arg1, "cacheKey") && isFunction(arg1.cacheKey)) {
const query = arg1;
validateQueryConfig(query);
const options = arg2 ?? {};
const params = options.params;
const cacheKey = query.cacheKey?.(params);
const url = query.url?.(params);
const queryFn = query.queryFn;
const schema = query.schema;
const placeholderData = options.placeholderData ?? query.placeholderData;
const fetchConfig = options.fetchConfig ?? query.fetchConfig;
const select = options.select ?? query.select;
const selectDeps = options.selectDeps ?? query.selectDeps;
const enabled = has(options, "enabled") ? options.enabled : isFunction(query.enabled) ? query.enabled(params) : query.enabled;
return _useQueryObserver({
...query,
...options,
enabled,
cacheKey,
url,
queryFn,
params,
schema,
placeholderData,
fetchConfig,
select,
selectDeps
});
}
return _useQueryObserver({
...arg1
});
}
function _useQueryObserver(options) {
validateQueryConfig(options);
const queryClient = useQueryClient();
const observerRef = useRef(void 0);
const defaultResultRef = useRef({
data: void 0,
error: void 0,
isLoading: true,
isFetching: true,
isError: false,
isSuccess: false,
isStale: true,
isPlaceholderData: false,
refetch: () => {
}
});
if (!observerRef.current) {
observerRef.current = new QueryObserver(queryClient, {
...options,
key: options.cacheKey
});
} else {
observerRef.current.setOptions({
...options,
key: options.cacheKey
});
}
const subscribe = useCallback((callback) => {
return observerRef.current.subscribe(callback);
}, []);
const getSnapshot = useCallback(() => {
if (!observerRef.current) {
return defaultResultRef.current;
}
return observerRef.current.getCurrentResult();
}, []);
const result = useSyncExternalStore(
subscribe,
getSnapshot,
getSnapshot
// getServerSnapshot도 동일하게
);
useEffect(() => {
observerRef.current?.start();
}, []);
useEffect(() => {
return () => {
observerRef.current?.destroy();
};
}, []);
return result;
}
var getInitialState = () => ({
data: void 0,
error: null,
isPending: false,
isSuccess: false,
isError: false
});
function useMutation(configOrOptions, overrideOptions) {
const isFactoryConfig = "url" in configOrOptions || "mutationFn" in configOrOptions;
if (isFactoryConfig && overrideOptions) {
const factoryConfig = configOrOptions;
const mergedOptions = mergeMutationOptions(factoryConfig, overrideOptions);
return _useMutationInternal(mergedOptions);
} else {
return _useMutationInternal(configOrOptions);
}
}
function mergeMutationOptions(factoryConfig, overrideOptions) {
const factoryOnMutate = factoryConfig.onMutate;
const factoryOnSuccess = factoryConfig.onSuccess;
const factoryOnError = factoryConfig.onError;
const factoryOnSettled = factoryConfig.onSettled;
const overrideOnMutate = overrideOptions.onMutate;
const overrideOnSuccess = overrideOptions.onSuccess;
const overrideOnError = overrideOptions.onError;
const overrideOnSettled = overrideOptions.onSettled;
return {
// Factory 기본 속성들
...factoryConfig,
// Override 옵션들로 덮어쓰기 (콜백 제외)
...overrideOptions,
// 콜백들은 양쪽 모두 실행하도록 병합
onMutate: combinedCallback(factoryOnMutate, overrideOnMutate),
onSuccess: combinedCallback(factoryOnSuccess, overrideOnSuccess),
onError: combinedCallback(factoryOnError, overrideOnError),
onSettled: combinedCallback(factoryOnSettled, overrideOnSettled)
};
}
function combinedCallback(first, second) {
if (!first && !second) return void 0;
if (!first) return second;
if (!second) return first;
return (...args) => {
const firstResult = first(...args);
const secondResult = second(...args);
if (firstResult && typeof firstResult.then === "function") {
return firstResult.then(() => secondResult);
}
return secondResult;
};
}
function _useMutationInternal(options) {
const queryClient = useQueryClient();
const fetcher = queryClient.getFetcher();
if (process.env.NODE_ENV !== "production") {
try {
validateMutationConfig(options);
} catch (error) {
throw error;
}
}
const [state, dispatch] = useReducer(
(prevState, action) => {
switch (action.type) {
case "MUTATE":
return {
...prevState,
isPending: true,
isSuccess: false,
isError: false,
error: null
};
case "SUCCESS":
return {
...prevState,
isPending: false,
isSuccess: true,
isError: false,
data: action.data,
error: null
};
case "ERROR":
return {
...prevState,
isPending: false,
isSuccess: false,
isError: true,
error: action.error
};
case "RESET":
return getInitialState();
default:
return prevState;
}
},
getInitialState()
);
const latestOptions = useRef(options);
latestOptions.current = options;
const getMutationFn = useCallback(() => {
if ("mutationFn" in options && options.mutationFn) {
return options.mutationFn;
}
return async (variables, fetcher2) => {
const urlBasedOptions = options;
const url = isFunction(urlBasedOptions.url) ? urlBasedOptions.url(variables) : urlBasedOptions.url;
const method = urlBasedOptions.method;
let dataForRequest = variables;
if (options.requestSchema) {
try {
dataForRequest = options.requestSchema.parse(variables);
} catch (e) {
if (e instanceof z.ZodError) {
const config = {
url,
method,
data: variables
};
const fetchError = new FetchError(
`Request validation failed: ${e.issues.map((issue) => issue.message).join(", ")}`,
config,
"ERR_VALIDATION"
);
fetchError.name = "ValidationError";
fetchError.cause = e;
fetchError.isValidationError = true;
throw fetchError;
}
throw e;
}
}
const requestConfig = merge(
{ schema: options.responseSchema },
// fetcher.defaults에서 baseURL을 가져와서 기본값으로 설정
{ baseURL: fetcher2.defaults.baseURL },
options.fetchConfig || {},
{
url,
method,
data: dataForRequest
}
);
const response = await fetcher2.request(requestConfig);
return response.data;
};
}, [options, fetcher]);
const mutateCallback = useCallback(
async (variables, mutateLocalOptions) => {
dispatch({ type: "MUTATE", variables });
let context;
try {
const onMutateCb = latestOptions.current.onMutate;
if (onMutateCb) {
context = await onMutateCb(variables);
}
const mutationFn = getMutationFn();
const data = await mutationFn(variables, fetcher);
dispatch({ type: "SUCCESS", data });
if (latestOptions.current.onSuccess) {
await latestOptions.current.onSuccess(data, variables, context);
}
if (mutateLocalOptions?.onSuccess) {
mutateLocalOptions.onSuccess(data, variables, context);
}
const invalidateQueriesOption = latestOptions.current.invalidateQueries;
if (invalidateQueriesOption) {
let keysToInvalidate;
if (isFunction(invalidateQueriesOption)) {
keysToInvalidate = invalidateQueriesOption(data, variables, context);
} else {
keysToInvalidate = invalidateQueriesOption;
}
if (isArray(keysToInvalidate)) {
keysToInvalidate.forEach((queryKey) => {
queryClient.invalidateQueries(queryKey);
});
}
}
if (latestOptions.current.onSettled) {
await latestOptions.current.onSettled(data, null, variables, context);
}
if (mutateLocalOptions?.onSettled) {
mutateLocalOptions.onSettled(data, null, variables, context);
}
return data;
} catch (err) {
const error = err;
dispatch({ type: "ERROR", error });
if (latestOptions.current.onError) {
await latestOptions.current.onError(error, variables, context);
}
if (mutateLocalOptions?.onError) {
mutateLocalOptions.onError(error, variables, context);
}
if (latestOptions.current.onSettled) {
await latestOptions.current.onSettled(void 0, error, variables, context);
}
if (mutateLocalOptions?.onSettled) {
mutateLocalOptions.onSettled(void 0, error, variables, context);
}
throw error;
}
},
[getMutationFn, queryClient, fetcher]
);
const mutate = useCallback(
(variables, localOptions) => {
mutateCallback(variables, localOptions).catch(() => {
});
},
[mutateCallback]
);
const mutateAsync = useCallback(
(variables, localOptions) => {
return mutateCallback(variables, localOptions);
},
[mutateCallback]
);
const reset = useCallback(() => {
dispatch({ type: "RESET" });
}, []);
return {
...state,
mutate,
mutateAsync,
reset
};
}
export { HydrationBoundary, QueryClientProvider, useMutation, useQuery, useQueryClient };
//# sourceMappingURL=react.mjs.map
//# sourceMappingURL=react.mjs.map