From 1b003bfc50aa6e98309c6f83cfdcc9f7c78bfba5 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Thu, 28 May 2026 17:49:30 +0200 Subject: [PATCH] fix(widgets): resolve hooks violations, stale state, and missing edit-mode guard - Move hooks above early returns in system-disks, immich-carousel, media-transcoding - Add enabled flag to immich-carousel query when albumId is absent - Replace disconnected local state in ping-indicator with query cache setData - Stabilize docker timestamp with useMemo to prevent useTimeAgo flicker - Wrap releases results in useMemo with proper type narrowing - Hide refresh indicator during edit mode --- .../widgets/src/app/ping/ping-indicator.tsx | 29 +++++++------------ packages/widgets/src/docker/component.tsx | 3 +- .../src/immich/album-carousel/component.tsx | 13 +++++---- .../src/media-transcoding/component.tsx | 3 +- packages/widgets/src/releases/component.tsx | 5 +++- .../widgets/src/system-disks/component.tsx | 10 +++---- 6 files changed, 30 insertions(+), 33 deletions(-) diff --git a/packages/widgets/src/app/ping/ping-indicator.tsx b/packages/widgets/src/app/ping/ping-indicator.tsx index 6ca1aaa2d..4ec836ca8 100644 --- a/packages/widgets/src/app/ping/ping-indicator.tsx +++ b/packages/widgets/src/app/ping/ping-indicator.tsx @@ -1,7 +1,5 @@ -import { useState } from "react"; import { IconCheck, IconX } from "@tabler/icons-react"; -import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; import { PingDot } from "./ping-dot"; @@ -11,40 +9,33 @@ interface PingIndicatorProps { } export const PingIndicator = ({ appId }: PingIndicatorProps) => { - const { data: ping } = clientApi.widget.app.ping.useQuery( - { - id: appId, - }, - { - refetchOnMount: false, - refetchOnWindowFocus: false, - }, + const { data: pingResult } = clientApi.widget.app.ping.useQuery( + { id: appId }, + { refetchOnMount: false, refetchOnWindowFocus: false }, ); - const [pingResult, setPingResult] = useState(ping); - + const utils = clientApi.useUtils(); clientApi.widget.app.updatedPing.useSubscription( { id: appId }, { onData(data) { - setPingResult(data); + utils.widget.app.ping.setData({ id: appId }, data); }, }, ); - const current = pingResult ?? ping; - if (!current) return null; + if (!pingResult) return null; - const isError = "error" in current || current.statusCode >= 500; + const isError = "error" in pingResult || pingResult.statusCode >= 500; return ( ); diff --git a/packages/widgets/src/docker/component.tsx b/packages/widgets/src/docker/component.tsx index 4a95c3989..7e6b8ef44 100644 --- a/packages/widgets/src/docker/component.tsx +++ b/packages/widgets/src/docker/component.tsx @@ -206,7 +206,8 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo const utils = clientApi.useUtils(); const { data } = clientApi.docker.getContainers.useQuery(); const containers = data?.containers ?? []; - const relativeTime = useTimeAgo(data?.timestamp ?? new Date()); + const timestamp = useMemo(() => data?.timestamp ?? new Date(), [data?.timestamp]); + const relativeTime = useTimeAgo(timestamp); clientApi.docker.subscribeContainers.useSubscription(undefined, { onData(data) { diff --git a/packages/widgets/src/immich/album-carousel/component.tsx b/packages/widgets/src/immich/album-carousel/component.tsx index c84ed7ae7..cbaa44e07 100644 --- a/packages/widgets/src/immich/album-carousel/component.tsx +++ b/packages/widgets/src/immich/album-carousel/component.tsx @@ -17,15 +17,18 @@ export default function ImmichAlbumCarouselWidget({ }: WidgetComponentProps<"immich-albumCarousel">) { const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0); + const { data: album } = clientApi.widget.immich.getAlbum.useQuery( + { + integrationId: integrationIds[0] ?? "", + albumId: options.albumId ?? "", + }, + { enabled: Boolean(options.albumId) }, + ); + if (!options.albumId) { return ; } - const { data: album } = clientApi.widget.immich.getAlbum.useQuery({ - integrationId: integrationIds[0] ?? "", - albumId: options.albumId, - }); - if (!album) return null; if (album.assets.length === 0) { diff --git a/packages/widgets/src/media-transcoding/component.tsx b/packages/widgets/src/media-transcoding/component.tsx index 8910de1ab..5ab18cd83 100644 --- a/packages/widgets/src/media-transcoding/component.tsx +++ b/packages/widgets/src/media-transcoding/component.tsx @@ -51,12 +51,11 @@ export default function MediaTranscodingWidget({ }); const [view, setView] = useState(options.defaultView); + const t = useI18n("widget.mediaTranscoding"); if (!transcodingData) return null; const totalQueuePages = Math.ceil((transcodingData.data.queue.totalCount || 1) / queuePageSize); - - const t = useI18n("widget.mediaTranscoding"); const isTiny = width < 256; return ( diff --git a/packages/widgets/src/releases/component.tsx b/packages/widgets/src/releases/component.tsx index 302c4b6c9..56e482a45 100644 --- a/packages/widgets/src/releases/component.tsx +++ b/packages/widgets/src/releases/component.tsx @@ -94,7 +94,10 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas ), ); - const results = queryResults.map((q) => q.data).filter(Boolean); + const results = useMemo( + () => queryResults.map((q) => q.data).filter((d): d is NonNullable => d != null), + [queryResults], + ); const repositories = useMemo(() => { const formattedResults = options.repositories diff --git a/packages/widgets/src/system-disks/component.tsx b/packages/widgets/src/system-disks/component.tsx index 3e254fdce..99ee275b4 100644 --- a/packages/widgets/src/system-disks/component.tsx +++ b/packages/widgets/src/system-disks/component.tsx @@ -39,11 +39,6 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo const scheme = useMantineColorScheme(); const t = useI18n(); - const lastItem = data.at(-1); - - if (!lastItem) return null; - const { fileSystem, smart } = lastItem.healthInfo; - clientApi.widget.healthMonitoring.subscribeSystemHealthStatus.useSubscription(queryInput, { onData(data) { utils.widget.healthMonitoring.getSystemHealthStatus.setData(queryInput, (oldData) => @@ -52,6 +47,11 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo }, }); + const lastItem = data.at(-1); + + if (!lastItem) return null; + const { fileSystem, smart } = lastItem.healthInfo; + if (fileSystem.length === 0) { throw new NoIntegrationDataError(); }