From f7b58f5ebd8ab4dd07a39bb7afa1c8a4743c8eb0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 1 Aug 2026 18:59:39 +0000 Subject: [PATCH] refactor(views): use react-query's isLoadingError instead of reimplementing it deriveQueryStatus previously inferred "never received data" by checking data !== undefined itself. TanStack Query already computes and exposes this exact distinction as isLoadingError (isError && no data) vs isRefetchError (isError && data present) on every useQuery result, so consume that directly instead of duplicating the logic. --- .../src/common/hooks-query/useCustomFields.ts | 4 ++-- .../src/common/hooks-query/useProjectData.ts | 10 ++++++++-- .../src/common/hooks-query/useRundown.ts | 20 +++++++++++++++---- .../src/common/hooks-query/useSettings.ts | 10 ++++++++-- .../src/common/hooks-query/useViewSettings.ts | 4 ++-- .../common/utils/__tests__/queryUtils.test.ts | 14 ++++++------- apps/client/src/common/utils/queryUtils.ts | 11 +++++----- 7 files changed, 48 insertions(+), 25 deletions(-) diff --git a/apps/client/src/common/hooks-query/useCustomFields.ts b/apps/client/src/common/hooks-query/useCustomFields.ts index d0a5fe028..92c679a70 100644 --- a/apps/client/src/common/hooks-query/useCustomFields.ts +++ b/apps/client/src/common/hooks-query/useCustomFields.ts @@ -9,12 +9,12 @@ import { deriveQueryStatus } from '../utils/queryUtils'; const placeholder: CustomFields = {}; export default function useCustomFields() { - const { data, status, isFetching, isError, refetch } = useQuery({ + const { data, status, isFetching, isError, isLoadingError, refetch } = useQuery({ queryKey: CUSTOM_FIELDS, queryFn: ({ signal }) => getCustomFields({ signal }), placeholderData: (previousData, _previousQuery) => previousData, refetchInterval: queryRefetchIntervalSlow, }); - return { data: data ?? placeholder, status: deriveQueryStatus(status, data), isFetching, isError, refetch }; + return { data: data ?? placeholder, status: deriveQueryStatus(status, isLoadingError), isFetching, isError, refetch }; } diff --git a/apps/client/src/common/hooks-query/useProjectData.ts b/apps/client/src/common/hooks-query/useProjectData.ts index 03a022e6f..ea7425e78 100644 --- a/apps/client/src/common/hooks-query/useProjectData.ts +++ b/apps/client/src/common/hooks-query/useProjectData.ts @@ -7,14 +7,20 @@ import { projectDataPlaceholder } from '../models/ProjectData'; import { deriveQueryStatus } from '../utils/queryUtils'; export default function useProjectData() { - const { data, status, isFetching, isError, refetch } = useQuery({ + const { data, status, isFetching, isError, isLoadingError, refetch } = useQuery({ queryKey: PROJECT_DATA, queryFn: ({ signal }) => getProjectData({ signal }), placeholderData: (previousData, _previousQuery) => previousData, refetchInterval: queryRefetchIntervalSlow, }); - return { data: data ?? projectDataPlaceholder, status: deriveQueryStatus(status, data), isFetching, isError, refetch }; + return { + data: data ?? projectDataPlaceholder, + status: deriveQueryStatus(status, isLoadingError), + isFetching, + isError, + refetch, + }; } export function useUpdateProjectData() { diff --git a/apps/client/src/common/hooks-query/useRundown.ts b/apps/client/src/common/hooks-query/useRundown.ts index 4a5cd599b..1b0ea42dd 100644 --- a/apps/client/src/common/hooks-query/useRundown.ts +++ b/apps/client/src/common/hooks-query/useRundown.ts @@ -33,7 +33,7 @@ export default function useRundown() { data: { loaded: loadedRundownId }, } = useProjectRundowns(); - const { data, status, isError, refetch, isFetching } = useQuery({ + const { data, status, isError, isLoadingError, refetch, isFetching } = useQuery({ queryKey: loadedRundownId ? getRundownQueryKey(loadedRundownId) : CURRENT_RUNDOWN_QUERY_KEY, queryFn: ({ signal }) => fetchCurrentRundown({ signal }), refetchInterval: queryRefetchIntervalSlow, @@ -51,7 +51,13 @@ export default function useRundown() { queryClient.removeQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true }); }, [loadedRundownId, queryClient]); - return { data: data ?? cachedRundownPlaceholder, status: deriveQueryStatus(status, data), isError, refetch, isFetching }; + return { + data: data ?? cachedRundownPlaceholder, + status: deriveQueryStatus(status, isLoadingError), + isError, + refetch, + isFetching, + }; } export function useRundownWithMetadata() { @@ -128,7 +134,7 @@ export function useRundownAuxData() { export function useRundownById(rundownId: string | null | undefined) { const enabled = Boolean(rundownId); - const { data, status, isError, refetch, isFetching } = useQuery({ + const { data, status, isError, isLoadingError, refetch, isFetching } = useQuery({ queryKey: getRundownQueryKey(rundownId ?? ''), queryFn: ({ signal }) => fetchRundown(rundownId!, { signal }), enabled, @@ -136,5 +142,11 @@ export function useRundownById(rundownId: string | null | undefined) { refetchInterval: queryRefetchIntervalSlow, }); - return { data: data ?? cachedRundownPlaceholder, status: deriveQueryStatus(status, data), isError, refetch, isFetching }; + return { + data: data ?? cachedRundownPlaceholder, + status: deriveQueryStatus(status, isLoadingError), + isError, + refetch, + isFetching, + }; } diff --git a/apps/client/src/common/hooks-query/useSettings.ts b/apps/client/src/common/hooks-query/useSettings.ts index d6103c50d..d0c6815ac 100644 --- a/apps/client/src/common/hooks-query/useSettings.ts +++ b/apps/client/src/common/hooks-query/useSettings.ts @@ -7,7 +7,7 @@ import { ontimePlaceholderSettings } from '../models/OntimeSettings'; import { deriveQueryStatus } from '../utils/queryUtils'; export default function useSettings() { - const { data, status, isFetching, isError, refetch } = useQuery({ + const { data, status, isFetching, isError, isLoadingError, refetch } = useQuery({ queryKey: APP_SETTINGS, queryFn: ({ signal }) => getSettings({ signal }), placeholderData: (previousData, _previousQuery) => previousData, @@ -23,5 +23,11 @@ export default function useSettings() { }, }); - return { data: data ?? ontimePlaceholderSettings, status: deriveQueryStatus(status, data), isFetching, isError, refetch }; + return { + data: data ?? ontimePlaceholderSettings, + status: deriveQueryStatus(status, isLoadingError), + isFetching, + isError, + refetch, + }; } diff --git a/apps/client/src/common/hooks-query/useViewSettings.ts b/apps/client/src/common/hooks-query/useViewSettings.ts index 05532674e..c901a2bec 100644 --- a/apps/client/src/common/hooks-query/useViewSettings.ts +++ b/apps/client/src/common/hooks-query/useViewSettings.ts @@ -8,7 +8,7 @@ import { viewsSettingsPlaceholder } from '../models/ViewSettings.type'; import { deriveQueryStatus } from '../utils/queryUtils'; export default function useViewSettings() { - const { data, status } = useQuery({ + const { data, status, isLoadingError } = useQuery({ queryKey: VIEW_SETTINGS, queryFn: ({ signal }) => getViewSettings({ signal }), placeholderData: (previousData, _previousQuery) => previousData, @@ -25,5 +25,5 @@ export default function useViewSettings() { }, }); - return { data: data ?? viewsSettingsPlaceholder, status: deriveQueryStatus(status, data), mutateAsync }; + return { data: data ?? viewsSettingsPlaceholder, status: deriveQueryStatus(status, isLoadingError), mutateAsync }; } diff --git a/apps/client/src/common/utils/__tests__/queryUtils.test.ts b/apps/client/src/common/utils/__tests__/queryUtils.test.ts index 65018ec8c..22a43b286 100644 --- a/apps/client/src/common/utils/__tests__/queryUtils.test.ts +++ b/apps/client/src/common/utils/__tests__/queryUtils.test.ts @@ -1,16 +1,14 @@ import { deriveQueryStatus } from '../queryUtils'; test('keeps pending and success statuses unchanged', () => { - expect(deriveQueryStatus('pending', undefined)).toBe('pending'); - expect(deriveQueryStatus('success', { some: 'data' })).toBe('success'); + expect(deriveQueryStatus('pending', false)).toBe('pending'); + expect(deriveQueryStatus('success', false)).toBe('success'); }); -test('keeps error status when there is no data', () => { - expect(deriveQueryStatus('error', undefined)).toBe('error'); +test('keeps error status on a genuine loading error (no data ever received)', () => { + expect(deriveQueryStatus('error', true)).toBe('error'); }); -test('downgrades error to success when data is still available', () => { - expect(deriveQueryStatus('error', { some: 'data' })).toBe('success'); - expect(deriveQueryStatus('error', [])).toBe('success'); - expect(deriveQueryStatus('error', 0)).toBe('success'); +test('downgrades error to success on a refetch error (data still available)', () => { + expect(deriveQueryStatus('error', false)).toBe('success'); }); diff --git a/apps/client/src/common/utils/queryUtils.ts b/apps/client/src/common/utils/queryUtils.ts index a4ee7e7fb..813f40470 100644 --- a/apps/client/src/common/utils/queryUtils.ts +++ b/apps/client/src/common/utils/queryUtils.ts @@ -1,12 +1,13 @@ import type { QueryStatus } from '@tanstack/react-query'; /** - * A background refetch failure still leaves the last successfully fetched data in place. - * In that case we want callers to keep treating the query as usable rather than erroring out, - * so we only report 'error' when we have never received data for this query. + * A background refetch failure (react-query's isRefetchError) still leaves the last + * successfully fetched data in place, so we want callers to keep treating the query as + * usable rather than erroring out. Only a genuine isLoadingError (never received data) + * should surface as 'error'. */ -export function deriveQueryStatus(status: QueryStatus, data: unknown): QueryStatus { - if (status === 'error' && data !== undefined) { +export function deriveQueryStatus(status: QueryStatus, isLoadingError: boolean): QueryStatus { + if (status === 'error' && !isLoadingError) { return 'success'; } return status;