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.
This commit is contained in:
Claude
2026-08-01 18:59:39 +00:00
parent 9e2dfaab16
commit f7b58f5ebd
7 changed files with 48 additions and 25 deletions
@@ -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 };
}
@@ -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() {
@@ -33,7 +33,7 @@ export default function useRundown() {
data: { loaded: loadedRundownId },
} = useProjectRundowns();
const { data, status, isError, refetch, isFetching } = useQuery<Rundown>({
const { data, status, isError, isLoadingError, refetch, isFetching } = useQuery<Rundown>({
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<Rundown>({
const { data, status, isError, isLoadingError, refetch, isFetching } = useQuery<Rundown>({
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,
};
}
@@ -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,
};
}
@@ -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 };
}
@@ -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');
});
+6 -5
View File
@@ -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;