fix: Prevent crashes on Services and ClickHouse dashboards (#1535)

Closes HDX-3125
Closes HDX-3126

# Summary

This PR fixes two application crashes due to infinite render loops: `useQueryState value updates --> set form values --> trigger form's useWatch/watch --> triggers useEffect --> calls setQueryState --> repeat...`.

In these cases, the fix is to compare useWatch values to the previous useWatch value (using usePrevious) and only call setQueryState when the form value has changed. Before, the useEffect was also called when the query state changes.
This commit is contained in:
Drew Davis 2025-12-30 11:05:20 -05:00 committed by GitHub
parent 103c63cc95
commit 4889205a86
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 22 additions and 7 deletions

View file

@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---
fix: Prevent crashes on Services and ClickHouse dashboards

View file

@ -12,7 +12,6 @@ import { format as formatSql } from '@hyperdx/common-utils/dist/sqlFormatter';
import { DisplayType } from '@hyperdx/common-utils/dist/types';
import {
Box,
BoxComponentProps,
Button,
Flex,
Grid,
@ -38,6 +37,7 @@ import OnboardingModal from './components/OnboardingModal';
import { useDashboardRefresh } from './hooks/useDashboardRefresh';
import { useConnections } from './connection';
import { parseTimeQuery, useNewTimeQuery } from './timeQuery';
import { usePrevious } from './utils';
// TODO: This is a hack to set the default time range
const defaultTimeRange = parseTimeQuery('Past 1h', false) as [Date, Date];
@ -443,12 +443,13 @@ function ClickhousePage() {
});
const watchedConnection = useWatch({ control, name: 'connection' });
const previousWatchedConnection = usePrevious(watchedConnection);
useEffect(() => {
if (watchedConnection !== connection) {
if (previousWatchedConnection !== watchedConnection) {
setConnection(watchedConnection ?? null);
}
}, [watchedConnection, connection, setConnection]);
}, [watchedConnection, setConnection, previousWatchedConnection]);
const DEFAULT_INTERVAL = 'Past 1h';
const [displayedTimeInputValue, setDisplayedTimeInputValue] =
useState(DEFAULT_INTERVAL);

View file

@ -75,6 +75,7 @@ import { IS_LOCAL_MODE } from './config';
import DashboardFilters from './DashboardFilters';
import DashboardFiltersModal from './DashboardFiltersModal';
import { HARD_LINES_LIMIT } from './HDXMultiSeriesTimeChart';
import { usePrevious } from './utils';
type AppliedConfigParams = {
source?: string | null;
@ -1431,7 +1432,11 @@ function ServicesDashboardPage() {
});
const service = useWatch({ control, name: 'service' });
const previousService = usePrevious(service);
const sourceId = useWatch({ control, name: 'source' });
const previousSourceId = usePrevious(sourceId);
const { data: source } = useSource({
id: sourceId,
});
@ -1500,18 +1505,22 @@ function ServicesDashboardPage() {
}, [handleSubmit, setAppliedConfigParams, onSearch, displayedTimeInputValue]);
// Auto-submit when source changes
// Note: do not include appliedConfig.source in the deps,
// to avoid infinite render loops when navigating away from the page
useEffect(() => {
if (sourceId && sourceId !== appliedConfig.source) {
if (sourceId && sourceId != previousSourceId) {
onSubmit();
}
}, [sourceId, appliedConfig.source, onSubmit]);
}, [sourceId, onSubmit, previousSourceId]);
// Auto-submit when service changes
// Note: do not include appliedConfig.service in the deps,
// to avoid infinite render loops when navigating away from the page
useEffect(() => {
if (service !== appliedConfig.service) {
if (service != previousService) {
onSubmit();
}
}, [service, appliedConfig.service, onSubmit]);
}, [service, onSubmit, previousService]);
return (
<Box p="sm">