refactor(views): keep query status truthful, push render logic to the view boundary

Reworks the previous fix, which had base hooks (useRundown, useSettings,
useCustomFields, useProjectData, useViewSettings) rewrite their own
status/isError to hide background-refetch failures. That was misleading:
those hooks are consumed well beyond the 9 view loaders, and other callers
may legitimately want to know a fetch genuinely failed.

Instead the base hooks now return react-query's status/isError untouched,
plus the isLoadingError flag react-query already computes (true only when
a query has never received data). aggregateQueryStatus (viewLoader.utils.ts)
takes {status, isLoadingError} pairs and only reports 'error' once every
query has settled and at least one never got data; a background/refetch
error with data present now aggregates to 'success'. The two single-query
consumers outside that aggregator (RundownList/CuesheetTable via
useScopedRundown, and ProjectInfo) branch on isLoadingError directly instead
of status === 'error'.

Removes the standalone deriveQueryStatus helper introduced in the prior
iteration, since the logic now lives in the one place that already existed
for this purpose (aggregateQueryStatus) rather than a new abstraction.
This commit is contained in:
Claude
2026-08-02 05:28:49 +00:00
parent f7b58f5ebd
commit f2455fec17
18 changed files with 143 additions and 123 deletions
@@ -4,7 +4,6 @@ import { CustomFields } from 'ontime-types';
import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { CUSTOM_FIELDS } from '../api/constants'; import { CUSTOM_FIELDS } from '../api/constants';
import { getCustomFields } from '../api/customFields'; import { getCustomFields } from '../api/customFields';
import { deriveQueryStatus } from '../utils/queryUtils';
const placeholder: CustomFields = {}; const placeholder: CustomFields = {};
@@ -16,5 +15,5 @@ export default function useCustomFields() {
refetchInterval: queryRefetchIntervalSlow, refetchInterval: queryRefetchIntervalSlow,
}); });
return { data: data ?? placeholder, status: deriveQueryStatus(status, isLoadingError), isFetching, isError, refetch }; return { data: data ?? placeholder, status, isFetching, isError, isLoadingError, refetch };
} }
@@ -4,7 +4,6 @@ import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { PROJECT_DATA } from '../api/constants'; import { PROJECT_DATA } from '../api/constants';
import { getProjectData, postProjectData } from '../api/project'; import { getProjectData, postProjectData } from '../api/project';
import { projectDataPlaceholder } from '../models/ProjectData'; import { projectDataPlaceholder } from '../models/ProjectData';
import { deriveQueryStatus } from '../utils/queryUtils';
export default function useProjectData() { export default function useProjectData() {
const { data, status, isFetching, isError, isLoadingError, refetch } = useQuery({ const { data, status, isFetching, isError, isLoadingError, refetch } = useQuery({
@@ -14,13 +13,7 @@ export default function useProjectData() {
refetchInterval: queryRefetchIntervalSlow, refetchInterval: queryRefetchIntervalSlow,
}); });
return { return { data: data ?? projectDataPlaceholder, status, isFetching, isError, isLoadingError, refetch };
data: data ?? projectDataPlaceholder,
status: deriveQueryStatus(status, isLoadingError),
isFetching,
isError,
refetch,
};
} }
export function useUpdateProjectData() { export function useUpdateProjectData() {
@@ -6,7 +6,6 @@ import { queryRefetchIntervalSlow } from '../../ontimeConfig';
import { CURRENT_RUNDOWN_QUERY_KEY, getRundownQueryKey } from '../api/constants'; import { CURRENT_RUNDOWN_QUERY_KEY, getRundownQueryKey } from '../api/constants';
import { fetchCurrentRundown, fetchRundown } from '../api/rundown'; import { fetchCurrentRundown, fetchRundown } from '../api/rundown';
import { useSelectedEventId } from '../hooks/useSocket'; import { useSelectedEventId } from '../hooks/useSocket';
import { deriveQueryStatus } from '../utils/queryUtils';
import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata'; import { ExtendedEntry, getFlatRundownMetadata, getRundownMetadata } from '../utils/rundownMetadata';
import { useProjectRundowns } from './useProjectRundowns'; import { useProjectRundowns } from './useProjectRundowns';
@@ -51,20 +50,14 @@ export default function useRundown() {
queryClient.removeQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true }); queryClient.removeQueries({ queryKey: CURRENT_RUNDOWN_QUERY_KEY, exact: true });
}, [loadedRundownId, queryClient]); }, [loadedRundownId, queryClient]);
return { return { data: data ?? cachedRundownPlaceholder, status, isError, isLoadingError, refetch, isFetching };
data: data ?? cachedRundownPlaceholder,
status: deriveQueryStatus(status, isLoadingError),
isError,
refetch,
isFetching,
};
} }
export function useRundownWithMetadata() { export function useRundownWithMetadata() {
const { data, status } = useRundown(); const { data, status, isLoadingError } = useRundown();
const selectedEventId = useSelectedEventId(); const selectedEventId = useSelectedEventId();
const rundownMetadata = useMemo(() => getRundownMetadata(data, selectedEventId), [data, selectedEventId]); const rundownMetadata = useMemo(() => getRundownMetadata(data, selectedEventId), [data, selectedEventId]);
return { data, status, rundownMetadata }; return { data, status, isLoadingError, rundownMetadata };
} }
/** /**
@@ -72,7 +65,7 @@ export function useRundownWithMetadata() {
* built from the order and rundown fields * built from the order and rundown fields
*/ */
export function useFlatRundown() { export function useFlatRundown() {
const { data, status } = useRundown(); const { data, status, isLoadingError } = useRundown();
const flatRundown = useMemo(() => { const flatRundown = useMemo(() => {
if (data.revision === -1) { if (data.revision === -1) {
@@ -81,15 +74,15 @@ export function useFlatRundown() {
return data.flatOrder.map((id) => data.entries[id]).filter((entry): entry is OntimeEntry => entry !== undefined); return data.flatOrder.map((id) => data.entries[id]).filter((entry): entry is OntimeEntry => entry !== undefined);
}, [data]); }, [data]);
return { data: flatRundown, rundownId: data.id, status }; return { data: flatRundown, rundownId: data.id, status, isLoadingError };
} }
export function useFlatRundownWithMetadata() { export function useFlatRundownWithMetadata() {
const { data, status } = useRundown(); const { data, status, isLoadingError } = useRundown();
const selectedEventId = useSelectedEventId(); const selectedEventId = useSelectedEventId();
const rundownWithMetadata = useMemo(() => getFlatRundownMetadata(data, selectedEventId), [data, selectedEventId]); const rundownWithMetadata = useMemo(() => getFlatRundownMetadata(data, selectedEventId), [data, selectedEventId]);
return { data: rundownWithMetadata, status }; return { data: rundownWithMetadata, status, isLoadingError };
} }
/** /**
@@ -142,11 +135,5 @@ export function useRundownById(rundownId: string | null | undefined) {
refetchInterval: queryRefetchIntervalSlow, refetchInterval: queryRefetchIntervalSlow,
}); });
return { return { data: data ?? cachedRundownPlaceholder, status, isError, isLoadingError, refetch, isFetching };
data: data ?? cachedRundownPlaceholder,
status: deriveQueryStatus(status, isLoadingError),
isError,
refetch,
isFetching,
};
} }
@@ -11,6 +11,7 @@ export type RundownSource = {
rundown: Rundown; rundown: Rundown;
flatRundown: ExtendedEntry[]; flatRundown: ExtendedEntry[];
status: string; status: string;
isLoadingError: boolean;
selectedEventId: EntryId | null; selectedEventId: EntryId | null;
}; };
@@ -35,7 +36,7 @@ function useRundownSource(rundownId: string | null, loadedRundownId: string | nu
const isLoadedTarget = rundownId !== null && rundownId === loadedRundownId; const isLoadedTarget = rundownId !== null && rundownId === loadedRundownId;
const runtimeSelectedEventId = useSelectedEventId(); const runtimeSelectedEventId = useSelectedEventId();
const effectiveSelectedEventId = isLoadedTarget ? runtimeSelectedEventId : null; const effectiveSelectedEventId = isLoadedTarget ? runtimeSelectedEventId : null;
const { data: rundown, status } = useRundownById(rundownId); const { data: rundown, status, isLoadingError } = useRundownById(rundownId);
const flatRundown = useMemo( const flatRundown = useMemo(
() => getFlatRundownMetadata(rundown, effectiveSelectedEventId), () => getFlatRundownMetadata(rundown, effectiveSelectedEventId),
[effectiveSelectedEventId, rundown], [effectiveSelectedEventId, rundown],
@@ -47,8 +48,9 @@ function useRundownSource(rundownId: string | null, loadedRundownId: string | nu
rundown, rundown,
flatRundown, flatRundown,
status, status,
isLoadingError,
selectedEventId: effectiveSelectedEventId, selectedEventId: effectiveSelectedEventId,
}), }),
[effectiveSelectedEventId, flatRundown, rundown, rundownId, status], [effectiveSelectedEventId, flatRundown, isLoadingError, rundown, rundownId, status],
); );
} }
@@ -4,7 +4,6 @@ import { unobfuscate } from 'ontime-utils';
import { APP_SETTINGS } from '../api/constants'; import { APP_SETTINGS } from '../api/constants';
import { getSettings } from '../api/settings'; import { getSettings } from '../api/settings';
import { ontimePlaceholderSettings } from '../models/OntimeSettings'; import { ontimePlaceholderSettings } from '../models/OntimeSettings';
import { deriveQueryStatus } from '../utils/queryUtils';
export default function useSettings() { export default function useSettings() {
const { data, status, isFetching, isError, isLoadingError, refetch } = useQuery({ const { data, status, isFetching, isError, isLoadingError, refetch } = useQuery({
@@ -23,11 +22,5 @@ export default function useSettings() {
}, },
}); });
return { return { data: data ?? ontimePlaceholderSettings, status, isFetching, isError, isLoadingError, refetch };
data: data ?? ontimePlaceholderSettings,
status: deriveQueryStatus(status, isLoadingError),
isFetching,
isError,
refetch,
};
} }
@@ -5,7 +5,6 @@ import { getViewSettings, postViewSettings } from '../../common/api/viewSettings
import { ontimeQueryClient } from '../../common/queryClient'; import { ontimeQueryClient } from '../../common/queryClient';
import { VIEW_SETTINGS } from '../api/constants'; import { VIEW_SETTINGS } from '../api/constants';
import { viewsSettingsPlaceholder } from '../models/ViewSettings.type'; import { viewsSettingsPlaceholder } from '../models/ViewSettings.type';
import { deriveQueryStatus } from '../utils/queryUtils';
export default function useViewSettings() { export default function useViewSettings() {
const { data, status, isLoadingError } = useQuery({ const { data, status, isLoadingError } = useQuery({
@@ -25,5 +24,5 @@ export default function useViewSettings() {
}, },
}); });
return { data: data ?? viewsSettingsPlaceholder, status: deriveQueryStatus(status, isLoadingError), mutateAsync }; return { data: data ?? viewsSettingsPlaceholder, status, isLoadingError, mutateAsync };
} }
@@ -1,14 +0,0 @@
import { deriveQueryStatus } from '../queryUtils';
test('keeps pending and success statuses unchanged', () => {
expect(deriveQueryStatus('pending', false)).toBe('pending');
expect(deriveQueryStatus('success', false)).toBe('success');
});
test('keeps error status on a genuine loading error (no data ever received)', () => {
expect(deriveQueryStatus('error', true)).toBe('error');
});
test('downgrades error to success on a refetch error (data still available)', () => {
expect(deriveQueryStatus('error', false)).toBe('success');
});
@@ -1,14 +0,0 @@
import type { QueryStatus } from '@tanstack/react-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, isLoadingError: boolean): QueryStatus {
if (status === 'error' && !isLoadingError) {
return 'success';
}
return status;
}
@@ -14,9 +14,18 @@ export interface OperatorData {
} }
export function useOperatorData(): ViewData<OperatorData> { export function useOperatorData(): ViewData<OperatorData> {
const { data: rundown, rundownMetadata, status: rundownStatus } = useRundownWithMetadata(); const {
const { data: customFields, status: customFieldStatus } = useCustomFields(); data: rundown,
const { data: settings, status: settingsStatus } = useSettings(); rundownMetadata,
status: rundownStatus,
isLoadingError: rundownIsLoadingError,
} = useRundownWithMetadata();
const {
data: customFields,
status: customFieldStatus,
isLoadingError: customFieldIsLoadingError,
} = useCustomFields();
const { data: settings, status: settingsStatus, isLoadingError: settingsIsLoadingError } = useSettings();
return { return {
data: { data: {
@@ -25,6 +34,10 @@ export function useOperatorData(): ViewData<OperatorData> {
customFields, customFields,
settings, settings,
}, },
status: aggregateQueryStatus([rundownStatus, customFieldStatus, settingsStatus]), status: aggregateQueryStatus([
{ status: rundownStatus, isLoadingError: rundownIsLoadingError },
{ status: customFieldStatus, isLoadingError: customFieldIsLoadingError },
{ status: settingsStatus, isLoadingError: settingsIsLoadingError },
]),
}; };
} }
@@ -8,7 +8,7 @@ import Rundown from './Rundown';
export default memo(RundownList); export default memo(RundownList);
function RundownList() { function RundownList() {
const { data, status, rundownMetadata } = useRundownWithMetadata(); const { data, status, isLoadingError, rundownMetadata } = useRundownWithMetadata();
const featureData = useRundownEditor(); const featureData = useRundownEditor();
const { getLocalizedString } = useTranslation(); const { getLocalizedString } = useTranslation();
@@ -17,7 +17,7 @@ function RundownList() {
return <EmptyFill text='Loading…' />; return <EmptyFill text='Loading…' />;
} }
if (status === 'error') { if (isLoadingError) {
return <EmptyFill text={getLocalizedString('common.no_data')} />; return <EmptyFill text={getLocalizedString('common.no_data')} />;
} }
@@ -20,10 +20,14 @@ export function useBackstageData(): ViewData<BackstageData> {
const isMirrored = useViewOptionsStore((state) => state.mirror); const isMirrored = useViewOptionsStore((state) => state.mirror);
// HTTP API data // HTTP API data
const { data: rundownData, status: rundownStatus } = useFlatRundown(); const { data: rundownData, status: rundownStatus, isLoadingError: rundownIsLoadingError } = useFlatRundown();
const { data: projectData, status: projectDataStatus } = useProjectData(); const { data: projectData, status: projectDataStatus, isLoadingError: projectDataIsLoadingError } = useProjectData();
const { data: settings, status: settingsStatus } = useSettings(); const { data: settings, status: settingsStatus, isLoadingError: settingsIsLoadingError } = useSettings();
const { data: customFields, status: customFieldsStatus } = useCustomFields(); const {
data: customFields,
status: customFieldsStatus,
isLoadingError: customFieldsIsLoadingError,
} = useCustomFields();
return { return {
data: { data: {
@@ -33,6 +37,11 @@ export function useBackstageData(): ViewData<BackstageData> {
isMirrored, isMirrored,
settings, settings,
}, },
status: aggregateQueryStatus([rundownStatus, projectDataStatus, settingsStatus, customFieldsStatus]), status: aggregateQueryStatus([
{ status: rundownStatus, isLoadingError: rundownIsLoadingError },
{ status: projectDataStatus, isLoadingError: projectDataIsLoadingError },
{ status: settingsStatus, isLoadingError: settingsIsLoadingError },
{ status: customFieldsStatus, isLoadingError: customFieldsIsLoadingError },
]),
}; };
} }
@@ -21,10 +21,18 @@ export function useCountdownData(): ViewData<CountdownData> {
const isMirrored = useViewOptionsStore((state) => state.mirror); const isMirrored = useViewOptionsStore((state) => state.mirror);
// HTTP API data // HTTP API data
const { data: rundownData, status: rundownStatus } = useFlatRundownWithMetadata(); const {
const { data: projectData, status: projectDataStatus } = useProjectData(); data: rundownData,
const { data: settings, status: settingsStatus } = useSettings(); status: rundownStatus,
const { data: customFields, status: customFieldsStatus } = useCustomFields(); isLoadingError: rundownIsLoadingError,
} = useFlatRundownWithMetadata();
const { data: projectData, status: projectDataStatus, isLoadingError: projectDataIsLoadingError } = useProjectData();
const { data: settings, status: settingsStatus, isLoadingError: settingsIsLoadingError } = useSettings();
const {
data: customFields,
status: customFieldsStatus,
isLoadingError: customFieldsIsLoadingError,
} = useCustomFields();
return { return {
data: { data: {
@@ -34,6 +42,11 @@ export function useCountdownData(): ViewData<CountdownData> {
isMirrored, isMirrored,
settings, settings,
}, },
status: aggregateQueryStatus([rundownStatus, projectDataStatus, settingsStatus, customFieldsStatus]), status: aggregateQueryStatus([
{ status: rundownStatus, isLoadingError: rundownIsLoadingError },
{ status: projectDataStatus, isLoadingError: projectDataIsLoadingError },
{ status: settingsStatus, isLoadingError: settingsIsLoadingError },
{ status: customFieldsStatus, isLoadingError: customFieldsIsLoadingError },
]),
}; };
} }
@@ -63,7 +63,7 @@ export default function CuesheetTable({
isCurrentRundown, isCurrentRundown,
insertElement, insertElement,
}: CuesheetTableProps) { }: CuesheetTableProps) {
const { flatRundown, status, selectedEventId } = source; const { flatRundown, status, isLoadingError, selectedEventId } = source;
const { updateEntry, updateTimer, addEntry } = useEntryActionsContext(); const { updateEntry, updateTimer, addEntry } = useEntryActionsContext();
const { getLocalizedString } = useTranslation(); const { getLocalizedString } = useTranslation();
const canCreateEntries = useCuesheetPermissions((state) => state.canCreateEntries) && cuesheetMode === AppMode.Edit; const canCreateEntries = useCuesheetPermissions((state) => state.canCreateEntries) && cuesheetMode === AppMode.Edit;
@@ -235,7 +235,7 @@ export default function CuesheetTable({
return <EmptyFill text='Loading…' className={style.tableLoading} />; return <EmptyFill text='Loading…' className={style.tableLoading} />;
} }
if (status === 'error') { if (isLoadingError) {
return <EmptyFill text={getLocalizedString('common.no_data')} className={style.tableLoading} />; return <EmptyFill text={getLocalizedString('common.no_data')} className={style.tableLoading} />;
} }
@@ -2,7 +2,7 @@ import { ProjectData } from 'ontime-types';
import useProjectData from '../../common/hooks-query/useProjectData'; import useProjectData from '../../common/hooks-query/useProjectData';
import { useViewOptionsStore } from '../../common/stores/viewOptions'; import { useViewOptionsStore } from '../../common/stores/viewOptions';
import { ViewData } from '../utils/viewLoader.utils'; import { ViewData, aggregateQueryStatus } from '../utils/viewLoader.utils';
export interface ProjectInfoData { export interface ProjectInfoData {
projectData: ProjectData; projectData: ProjectData;
@@ -14,13 +14,13 @@ export function useProjectInfoData(): ViewData<ProjectInfoData> {
const isMirrored = useViewOptionsStore((state) => state.mirror); const isMirrored = useViewOptionsStore((state) => state.mirror);
// HTTP API data // HTTP API data
const { data: projectData, status: projectDataStatus } = useProjectData(); const { data: projectData, status: projectDataStatus, isLoadingError: projectDataIsLoadingError } = useProjectData();
return { return {
data: { data: {
projectData, projectData,
isMirrored, isMirrored,
}, },
status: projectDataStatus, status: aggregateQueryStatus([{ status: projectDataStatus, isLoadingError: projectDataIsLoadingError }]),
}; };
} }
+18 -5
View File
@@ -20,10 +20,18 @@ export function useStudioData(): ViewData<StudioData> {
const isMirrored = useViewOptionsStore((state) => state.mirror); const isMirrored = useViewOptionsStore((state) => state.mirror);
// HTTP API data // HTTP API data
const { data: projectData, status: projectDataStatus } = useProjectData(); const { data: projectData, status: projectDataStatus, isLoadingError: projectDataIsLoadingError } = useProjectData();
const { data: viewSettings, status: viewSettingsStatus } = useViewSettings(); const {
const { data: settings, status: settingsStatus } = useSettings(); data: viewSettings,
const { data: customFields, status: customFieldsStatus } = useCustomFields(); status: viewSettingsStatus,
isLoadingError: viewSettingsIsLoadingError,
} = useViewSettings();
const { data: settings, status: settingsStatus, isLoadingError: settingsIsLoadingError } = useSettings();
const {
data: customFields,
status: customFieldsStatus,
isLoadingError: customFieldsIsLoadingError,
} = useCustomFields();
return { return {
data: { data: {
@@ -33,6 +41,11 @@ export function useStudioData(): ViewData<StudioData> {
settings, settings,
viewSettings, viewSettings,
}, },
status: aggregateQueryStatus([projectDataStatus, viewSettingsStatus, settingsStatus, customFieldsStatus]), status: aggregateQueryStatus([
{ status: projectDataStatus, isLoadingError: projectDataIsLoadingError },
{ status: viewSettingsStatus, isLoadingError: viewSettingsIsLoadingError },
{ status: settingsStatus, isLoadingError: settingsIsLoadingError },
{ status: customFieldsStatus, isLoadingError: customFieldsIsLoadingError },
]),
}; };
} }
@@ -16,10 +16,18 @@ export interface TimelineData {
export function useTimelineData(): ViewData<TimelineData> { export function useTimelineData(): ViewData<TimelineData> {
// HTTP API data // HTTP API data
const { data: rundownData, status: rundownStatus } = useFlatRundownWithMetadata(); const {
const { data: projectData, status: projectDataStatus } = useProjectData(); data: rundownData,
const { data: settings, status: settingsStatus } = useSettings(); status: rundownStatus,
const { data: customFields, status: customFieldsStatus } = useCustomFields(); isLoadingError: rundownIsLoadingError,
} = useFlatRundownWithMetadata();
const { data: projectData, status: projectDataStatus, isLoadingError: projectDataIsLoadingError } = useProjectData();
const { data: settings, status: settingsStatus, isLoadingError: settingsIsLoadingError } = useSettings();
const {
data: customFields,
status: customFieldsStatus,
isLoadingError: customFieldsIsLoadingError,
} = useCustomFields();
return { return {
data: { data: {
@@ -28,6 +36,11 @@ export function useTimelineData(): ViewData<TimelineData> {
projectData, projectData,
settings, settings,
}, },
status: aggregateQueryStatus([rundownStatus, projectDataStatus, settingsStatus, customFieldsStatus]), status: aggregateQueryStatus([
{ status: rundownStatus, isLoadingError: rundownIsLoadingError },
{ status: projectDataStatus, isLoadingError: projectDataIsLoadingError },
{ status: settingsStatus, isLoadingError: settingsIsLoadingError },
{ status: customFieldsStatus, isLoadingError: customFieldsIsLoadingError },
]),
}; };
} }
+18 -10
View File
@@ -22,11 +22,19 @@ export function useTimerData(): ViewData<TimerData> {
const isMirrored = useViewOptionsStore((state) => state.mirror); const isMirrored = useViewOptionsStore((state) => state.mirror);
// HTTP API data // HTTP API data
const { data: projectData, status: projectDataStatus } = useProjectData(); const { data: projectData, status: projectDataStatus, isLoadingError: projectDataIsLoadingError } = useProjectData();
const { data: viewSettings, status: viewSettingsStatus } = useViewSettings(); const {
const { data: settings, status: settingsStatus } = useSettings(); data: viewSettings,
const { data: customFields, status: customFieldsStatus } = useCustomFields(); status: viewSettingsStatus,
const { data: rundown, status: rundownStatus } = useRundown(); isLoadingError: viewSettingsIsLoadingError,
} = useViewSettings();
const { data: settings, status: settingsStatus, isLoadingError: settingsIsLoadingError } = useSettings();
const {
data: customFields,
status: customFieldsStatus,
isLoadingError: customFieldsIsLoadingError,
} = useCustomFields();
const { data: rundown, status: rundownStatus, isLoadingError: rundownIsLoadingError } = useRundown();
const { entries } = rundown; const { entries } = rundown;
return { return {
@@ -39,11 +47,11 @@ export function useTimerData(): ViewData<TimerData> {
entries, entries,
}, },
status: aggregateQueryStatus([ status: aggregateQueryStatus([
projectDataStatus, { status: projectDataStatus, isLoadingError: projectDataIsLoadingError },
viewSettingsStatus, { status: viewSettingsStatus, isLoadingError: viewSettingsIsLoadingError },
settingsStatus, { status: settingsStatus, isLoadingError: settingsIsLoadingError },
customFieldsStatus, { status: customFieldsStatus, isLoadingError: customFieldsIsLoadingError },
rundownStatus, { status: rundownStatus, isLoadingError: rundownIsLoadingError },
]), ]),
}; };
} }
+17 -11
View File
@@ -5,21 +5,27 @@ export type ViewData<T> = {
status: QueryStatus; status: QueryStatus;
}; };
type AggregatableQuery = {
status: QueryStatus;
/** true only when the query has never received data, ie useQuery's isLoadingError */
isLoadingError: boolean;
};
/** /**
* Aggregates a loading status from multiple query statuses. * Aggregates a loading status from multiple queries, for the purpose of deciding
* If all statuses are 'pending', returns 'pending'. * whether a view can render.
* If all statuses are 'success', returns 'success'. * - 'pending' while any query hasn't settled yet (no result, first fetch in flight)
* If any status is 'error', returns 'error'. * - 'error' once all queries have settled, if any of them never received data
* - 'success' once all queries have settled and every one has data to show,
* even if a query's last (background) fetch failed
*/ */
export function aggregateQueryStatus(statuses: QueryStatus[]): QueryStatus { export function aggregateQueryStatus(queries: AggregatableQuery[]): QueryStatus {
if (statuses.every((status) => status === 'pending')) { const allSettled = queries.every((query) => query.status !== 'pending');
if (!allSettled) {
return 'pending'; return 'pending';
} }
if (statuses.every((status) => status === 'success')) { if (queries.some((query) => query.isLoadingError)) {
return 'success';
}
if (statuses.some((status) => status === 'error')) {
return 'error'; return 'error';
} }
return 'pending'; return 'success';
} }