From ac73b9d891fc5d4f8eb192e53c705738a2bb080b Mon Sep 17 00:00:00 2001 From: Clarkar00 <133609856+Clarkar00@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:12:03 -0700 Subject: [PATCH] fix(widgets): refetch after render errors (#6399) Remove incompatible persisted widget query data before resetting a crashed widget so it can fetch fresh data instead of re-rendering the same stale payload. --- .../components/board/items/item-content.tsx | 3 +- .../board/items/widget-query-recovery.spec.ts | 54 +++++++++++++++++++ .../board/items/widget-query-recovery.ts | 7 +++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 apps/nextjs/src/components/board/items/widget-query-recovery.spec.ts create mode 100644 apps/nextjs/src/components/board/items/widget-query-recovery.ts diff --git a/apps/nextjs/src/components/board/items/item-content.tsx b/apps/nextjs/src/components/board/items/item-content.tsx index d869538e4..ad0f3a305 100644 --- a/apps/nextjs/src/components/board/items/item-content.tsx +++ b/apps/nextjs/src/components/board/items/item-content.tsx @@ -19,6 +19,7 @@ import { useItemActions } from "./item-actions"; import itemContentClasses from "./item-content.module.css"; import { BoardItemMenu } from "./item-menu"; import { WidgetContextMenu } from "./widget-context-menu"; +import { removePersistedWidgetQueries } from "./widget-query-recovery"; interface BoardItemContentProps { item: SectionItem; @@ -112,7 +113,7 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => { {({ reset }) => ( { - void queryClient.invalidateQueries({ queryKey: [["widget"]] }); + removePersistedWidgetQueries(queryClient); reset(); }} fallbackRender={({ resetErrorBoundary, error }) => ( diff --git a/apps/nextjs/src/components/board/items/widget-query-recovery.spec.ts b/apps/nextjs/src/components/board/items/widget-query-recovery.spec.ts new file mode 100644 index 000000000..b666df4e9 --- /dev/null +++ b/apps/nextjs/src/components/board/items/widget-query-recovery.spec.ts @@ -0,0 +1,54 @@ +import { QueryClient } from "@tanstack/react-query"; +import { describe, expect, test } from "vitest"; + +import { removePersistedWidgetQueries } from "./widget-query-recovery"; + +describe("removePersistedWidgetQueries", () => { + test("removes stale widget queries and allows fresh data to be fetched", async () => { + const queryClient = new QueryClient(); + const crashedWidgetQueryKey = [["widget", "mediaServer", "getCurrentStreams"], { input: {} }]; + const unrelatedQueryKey = [["user", "me"], { input: {} }]; + + queryClient.setQueryData(crashedWidgetQueryKey, "stale"); + queryClient.setQueryData(unrelatedQueryKey, "unrelated"); + + removePersistedWidgetQueries(queryClient); + + expect(queryClient.getQueryData(crashedWidgetQueryKey)).toBeUndefined(); + expect(queryClient.getQueryData(unrelatedQueryKey)).toBe("unrelated"); + await expect( + queryClient.fetchQuery({ queryKey: crashedWidgetQueryKey, queryFn: () => Promise.resolve("fresh") }), + ).resolves.toBe("fresh"); + }); + + test("removes persisted queries outside the widget router", () => { + const queryClient = new QueryClient(); + const dockerQueryKey = [["docker", "getContainers"], { input: {} }]; + const appQueryKey = [["app", "byId"], { input: {} }]; + const unrelatedQueryKey = [["user", "me"], { input: {} }]; + + queryClient.setQueryData(dockerQueryKey, "stale"); + queryClient.setQueryData(appQueryKey, "stale"); + queryClient.setQueryData(unrelatedQueryKey, "unrelated"); + + removePersistedWidgetQueries(queryClient); + + expect(queryClient.getQueryData(dockerQueryKey)).toBeUndefined(); + expect(queryClient.getQueryData(appQueryKey)).toBeUndefined(); + expect(queryClient.getQueryData(unrelatedQueryKey)).toBe("unrelated"); + }); + + test("removes persisted widget queries when the widget kind and query path differ", () => { + const queryClient = new QueryClient(); + const crashedWidgetQueryKey = [["widget", "anchorNotes", "getNote"], { input: {} }]; + const unrelatedQueryKey = [["user", "me"], { input: {} }]; + + queryClient.setQueryData(crashedWidgetQueryKey, "stale"); + queryClient.setQueryData(unrelatedQueryKey, "unrelated"); + + removePersistedWidgetQueries(queryClient); + + expect(queryClient.getQueryData(crashedWidgetQueryKey)).toBeUndefined(); + expect(queryClient.getQueryData(unrelatedQueryKey)).toBe("unrelated"); + }); +}); diff --git a/apps/nextjs/src/components/board/items/widget-query-recovery.ts b/apps/nextjs/src/components/board/items/widget-query-recovery.ts new file mode 100644 index 000000000..800614bf4 --- /dev/null +++ b/apps/nextjs/src/components/board/items/widget-query-recovery.ts @@ -0,0 +1,7 @@ +import type { QueryClient } from "@tanstack/react-query"; + +import { isPersistableWidgetQueryKey } from "@homarr/api/query-cache"; + +export const removePersistedWidgetQueries = (queryClient: QueryClient) => { + queryClient.removeQueries({ predicate: (query) => isPersistableWidgetQueryKey(query.queryKey) }); +};