feat: simplify caching — TanStack Query as single source of truth (#6075)

This commit is contained in:
Thomas authored and GitHub committed 2026-07-03 16:33:03 +02:00
1 parent a5ec880eec
commit f19033e0d5
187 files changed
+1013 -4712

No files matched your search

@@ -2,7 +2,7 @@ import type { MutableRefObject } from "react";
import { useRef } from "react";
import { Badge, Card } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
import { QueryErrorResetBoundary } from "@tanstack/react-query";
import { QueryErrorResetBoundary, useQueryClient } from "@tanstack/react-query";
import combineClasses from "clsx";
import { NoIntegrationSelectedError } from "@homarr/widgets/errors/classes";
import { ErrorBoundary } from "react-error-boundary";
@@ -105,12 +105,16 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
updateItemOptions({ itemId: item.id, newOptions });
const widgetSupportsIntegrations =
"supportedIntegrations" in definition && definition.supportedIntegrations.length >= 1;
const queryClient = useQueryClient();
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
onReset={() => {
void queryClient.invalidateQueries({ queryKey: [["widget"]] });
reset();
}}
fallbackRender={({ resetErrorBoundary, error }) => (
<>
<BoardItemMenu offset={4} item={newItem} resetErrorBoundary={resetErrorBoundary} />
@@ -1,9 +1,11 @@
"use client";
import type { MutableRefObject, ReactNode } from "react";
import { useCallback, useMemo } from "react";
import { Group, Menu, Switch } from "@mantine/core";
import { IconCopy, IconLayoutKanban, IconSettings, IconTrash } from "@tabler/icons-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Group, Menu, Switch, Text } from "@mantine/core";
import { IconCopy, IconLayoutKanban, IconRefresh, IconSettings, IconTrash } from "@tabler/icons-react";
import type { QueryClient } from "@tanstack/react-query";
import { useQueryClient } from "@tanstack/react-query";
import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
@@ -45,6 +47,12 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont
const { mutate: saveBoard } = clientApi.board.saveBoard.useMutation();
const currentDefinition = useMemo(() => widgetImports[item.kind].definition, [item.kind]);
const { gridstack } = useSectionContext().refs;
const queryClient = useQueryClient();
const widgetQueryKey = useMemo(() => [["widget", item.kind]], [item.kind]);
const handleRefetch = useCallback(() => {
void queryClient.invalidateQueries({ queryKey: widgetQueryKey });
}, [queryClient, widgetQueryKey]);
const options = useMemo(
() => reduceWidgetOptionsWithDefaultValues(item.kind, settings, item.options) as Record<string, unknown>,
@@ -214,7 +222,15 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont
)}
<>
{!isEditMode && (toggleOptions.length > 0 || visibleWidgetActions.length > 0) && <Menu.Divider />}
{(toggleOptions.length > 0 || visibleWidgetActions.length > 0 || isEditMode) && <Menu.Divider />}
<Menu.Item closeMenuOnClick leftSection={<IconRefresh size={16} />} onClick={handleRefetch}>
<Group justify="space-between" wrap="nowrap">
{tMenu("refresh")}
<Text size="xs" c="dimmed">
<WidgetCacheAge queryClient={queryClient} queryKey={widgetQueryKey} />
</Text>
</Group>
</Menu.Item>
<Menu.Item
closeMenuOnClick
leftSection={<IconSettings size={16} />}
@@ -248,3 +264,24 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont
</Menu>
);
};
const WidgetCacheAge = ({ queryClient, queryKey }: { queryClient: QueryClient; queryKey: unknown[] }) => {
const [, setTick] = useState(0);
useEffect(() => {
const id = setInterval(() => setTick((n) => n + 1), 1000);
return () => clearInterval(id);
}, []);
const timestamps = queryClient
.getQueryCache()
.findAll({ queryKey })
.map((q) => q.state.dataUpdatedAt)
.filter(Boolean);
if (timestamps.length === 0) return null;
const oldest = Math.min(...timestamps);
const seconds = Math.floor((Date.now() - oldest) / 1000);
if (seconds < 5) return "just now";
if (seconds < 60) return `${seconds}s ago`;
return `${Math.floor(seconds / 60)}m ago`;
};