|
@@ -0,0 +1,70 @@
|
|
|
|
|
+import React from "react";
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * useExplorerQuery
|
|
|
|
|
+ *
|
|
|
|
|
+ * A tiny, predictable data-fetching hook for the Explorer UI.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Design goals:
|
|
|
|
|
+ * - Keep logic centralized and consistent across all Explorer levels.
|
|
|
|
|
+ * - Provide stable states: loading / success / error.
|
|
|
|
|
+ * - Provide a retry mechanism without leaking implementation details to the UI.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Non-goals:
|
|
|
|
|
+ * - This hook does NOT perform routing/redirects.
|
|
|
|
|
+ * Routing decisions remain in the UI components for clarity.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @template T
|
|
|
|
|
+ * @param {() => Promise<T>} loadFn - async loader (e.g. apiClient.getYears)
|
|
|
|
|
+ * @param {any[]} deps - dependencies that define the query identity (e.g. [branch, year])
|
|
|
|
|
+ * @returns {{
|
|
|
|
|
+ * status: "loading"|"success"|"error",
|
|
|
|
|
+ * data: T|null,
|
|
|
|
|
+ * error: unknown|null,
|
|
|
|
|
+ * retry: () => void
|
|
|
|
|
+ * }}
|
|
|
|
|
+ */
|
|
|
|
|
+export function useExplorerQuery(loadFn, deps) {
|
|
|
|
|
+ const [status, setStatus] = React.useState("loading");
|
|
|
|
|
+ const [data, setData] = React.useState(null);
|
|
|
|
|
+ const [error, setError] = React.useState(null);
|
|
|
|
|
+
|
|
|
|
|
+ // Incrementing this tick triggers a refetch without changing deps.
|
|
|
|
|
+ const [retryTick, setRetryTick] = React.useState(0);
|
|
|
|
|
+
|
|
|
|
|
+ const retry = React.useCallback(() => {
|
|
|
|
|
+ setRetryTick((n) => n + 1);
|
|
|
|
|
+ }, []);
|
|
|
|
|
+
|
|
|
|
|
+ React.useEffect(() => {
|
|
|
|
|
+ let cancelled = false;
|
|
|
|
|
+
|
|
|
|
|
+ async function run() {
|
|
|
|
|
+ setStatus("loading");
|
|
|
|
|
+ setError(null);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const result = await loadFn();
|
|
|
|
|
+ if (cancelled) return;
|
|
|
|
|
+
|
|
|
|
|
+ setData(result);
|
|
|
|
|
+ setStatus("success");
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ if (cancelled) return;
|
|
|
|
|
+
|
|
|
|
|
+ setData(null);
|
|
|
|
|
+ setError(err);
|
|
|
|
|
+ setStatus("error");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ run();
|
|
|
|
|
+
|
|
|
|
|
+ return () => {
|
|
|
|
|
+ cancelled = true;
|
|
|
|
|
+ };
|
|
|
|
|
+ // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
+ }, [...deps, retryTick]);
|
|
|
|
|
+
|
|
|
|
|
+ return { status, data, error, retry };
|
|
|
|
|
+}
|