Skip to content

Commit 5c9d995

Browse files
committed
Fix for useFetcher errors bubbling up to the web inspector
1 parent 7bd7370 commit 5c9d995

1 file changed

Lines changed: 51 additions & 16 deletions

File tree

apps/webapp/app/routes/resources.metric.tsx

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { OutputColumnMetadata } from "@internal/clickhouse";
2-
import { useFetcher } from "@remix-run/react";
32
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
4-
import { useCallback, useEffect } from "react";
3+
import { useCallback, useEffect, useRef, useState } from "react";
54
import { z } from "zod";
65
import { requireUserId } from "~/services/session.server";
76
import { hasAccessToEnvironment } from "~/models/runtimeEnvironment.server";
@@ -164,24 +163,60 @@ export function MetricWidget({
164163
onDuplicate,
165164
...props
166165
}: MetricWidgetProps) {
167-
const fetcher = useFetcher<MetricWidgetActionResponse>();
168-
const isLoading = fetcher.state !== "idle";
166+
const [response, setResponse] = useState<MetricWidgetActionResponse | null>(null);
167+
const [isLoading, setIsLoading] = useState(false);
168+
const abortControllerRef = useRef<AbortController | null>(null);
169169

170-
const submit = useCallback(async () => {
171-
fetcher.submit(props, {
170+
// Track the latest props so the submit callback always uses fresh values
171+
// without needing to be recreated (which would cause useInterval to re-register listeners).
172+
const propsRef = useRef(props);
173+
propsRef.current = props;
174+
175+
const submit = useCallback(() => {
176+
// Abort any in-flight request for this widget
177+
abortControllerRef.current?.abort();
178+
179+
const controller = new AbortController();
180+
abortControllerRef.current = controller;
181+
setIsLoading(true);
182+
183+
fetch(`/resources/metric`, {
172184
method: "POST",
173-
action: `/resources/metric`,
174-
encType: "application/json",
175-
});
176-
}, [JSON.stringify(props)]);
185+
headers: { "Content-Type": "application/json" },
186+
body: JSON.stringify(propsRef.current),
187+
signal: controller.signal,
188+
})
189+
.then((res) => res.json() as Promise<MetricWidgetActionResponse>)
190+
.then((data) => {
191+
if (!controller.signal.aborted) {
192+
setResponse(data);
193+
setIsLoading(false);
194+
}
195+
})
196+
.catch((err) => {
197+
// Ignore aborted requests
198+
if (err instanceof DOMException && err.name === "AbortError") return;
199+
if (!controller.signal.aborted) {
200+
setIsLoading(false);
201+
}
202+
});
203+
}, []);
204+
205+
// Clean up on unmount
206+
useEffect(() => {
207+
return () => {
208+
abortControllerRef.current?.abort();
209+
};
210+
}, []);
177211

178-
// Reload periodically and on focus
179-
useInterval({ interval: refreshIntervalMs, callback: submit });
212+
// Reload periodically and on focus (onLoad: false — the useEffect below handles initial load)
213+
useInterval({ interval: refreshIntervalMs, callback: submit, onLoad: false });
180214

181-
// Reload when query, time period, or filters change
215+
// Reload on mount and when query, time period, or filters change
182216
useEffect(() => {
183217
submit();
184218
}, [
219+
submit,
185220
props.query,
186221
props.from,
187222
props.to,
@@ -191,8 +226,8 @@ export function MetricWidget({
191226
JSON.stringify(props.queues),
192227
]);
193228

194-
const data = fetcher.data?.success
195-
? { rows: fetcher.data.data.rows, columns: fetcher.data.data.columns }
229+
const data = response?.success
230+
? { rows: response.data.rows, columns: response.data.columns }
196231
: { rows: [], columns: [] };
197232

198233
return (
@@ -202,7 +237,7 @@ export function MetricWidget({
202237
config={config}
203238
isLoading={isLoading}
204239
data={data}
205-
error={fetcher.data?.success === false ? fetcher.data.error : undefined}
240+
error={response?.success === false ? response.error : undefined}
206241
isResizing={isResizing}
207242
isDraggable={isDraggable}
208243
onEdit={onEdit}

0 commit comments

Comments
 (0)