chore: add lint rules to treat missing hook dependencies as errors (#1420)

Tested on each of the spots that had hooks that were changed, seems good
This commit is contained in:
Aaron Knudtson 2025-12-01 19:12:46 -05:00 committed by GitHub
parent 991bd7e615
commit 815e6424a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 154 additions and 105 deletions

View file

@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---
chore: treat missing react hook dependencies as errors

View file

@ -1,7 +1,12 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['simple-import-sort', '@typescript-eslint', 'prettier'],
plugins: [
'simple-import-sort',
'@typescript-eslint',
'prettier',
'react-hooks',
],
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
@ -23,6 +28,7 @@ module.exports = {
'react/display-name': 'off',
'simple-import-sort/exports': 'error',
'simple-import-sort/imports': 'error',
'react-hooks/exhaustive-deps': 'error',
'no-console': ['error', { allow: ['warn', 'error'] }],
},
overrides: [

View file

@ -363,7 +363,7 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
isLoading: isLogViewsLoading,
refetch: refetchLogViews,
} = useSavedSearches();
const logViews = logViewsData ?? [];
const logViews = useMemo(() => logViewsData ?? [], [logViewsData]);
const updateDashboard = useUpdateDashboard();
const updateLogView = useUpdateSavedSearch();
@ -373,7 +373,7 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
isLoading: isDashboardsLoading,
refetch: refetchDashboards,
} = useDashboards();
const dashboards = dashboardsData ?? [];
const dashboards = useMemo(() => dashboardsData ?? [], [dashboardsData]);
const router = useRouter();
const { pathname, query } = router;

View file

@ -176,13 +176,13 @@ function BenchmarkPage() {
) {
return;
}
setQueries(data.queries);
setConnections(data.connections);
setQueries(data.queries || []);
setConnections(data.connections || []);
setIterations(data.iterations);
};
const _queries = queries || [];
const _connections = connections || [];
const _queries = useMemo(() => queries || [], [queries]);
const _connections = useMemo(() => connections || [], [connections]);
const { data: estimates } = useEstimates(
{ queries: _queries, connections: _connections },

View file

@ -149,7 +149,7 @@ const Tile = forwardRef(
.getElementById(`chart-${chart.id}`)
?.scrollIntoView({ behavior: 'smooth' });
}
}, []);
}, [chart.id, isHighlighed]);
const [queriedConfig, setQueriedConfig] = useState<
ChartConfigWithDateRange | undefined
@ -776,6 +776,7 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
where,
whereLanguage,
onTimeRangeSelect,
filterQueries,
],
);

View file

@ -530,6 +530,7 @@ function useSearchedConfigToChartConfig({
where,
whereLanguage,
defaultOrderBy,
orderBy,
]);
}
@ -1072,7 +1073,7 @@ function DBSearchPage() {
...chartConfig,
dateRange: searchedTimeRange,
};
}, [me?.team, chartConfig, searchedTimeRange]);
}, [chartConfig, searchedTimeRange]);
const displayedColumns = splitAndTrimWithBracket(
dbSqlRowTableConfig?.select ??
@ -1243,7 +1244,7 @@ function DBSearchPage() {
onTimeRangeSelect(d1, d2);
setIsLive(false);
},
[onTimeRangeSelect],
[onTimeRangeSelect, setIsLive],
);
const filtersChartConfig = useMemo<ChartConfigWithDateRange>(() => {

View file

@ -243,7 +243,7 @@ export default function DOMPlayer({
}, [setPlayerTime]);
const updatePlayerTimeRafRef = useRef(0);
const updatePlayerTime = () => {
const updatePlayerTime = useCallback(() => {
if (
replayer.current != null &&
replayer.current.service.state.matches('playing')
@ -257,7 +257,7 @@ export default function DOMPlayer({
}
updatePlayerTimeRafRef.current = requestAnimationFrame(updatePlayerTime);
};
}, []);
// Update timestamp ui in timeline
useEffect(() => {
@ -265,7 +265,7 @@ export default function DOMPlayer({
return () => {
cancelAnimationFrame(updatePlayerTimeRafRef.current);
};
}, []);
}, [updatePlayerTime]);
// Manage playback pause/play state, rrweb only
useEffect(() => {
@ -398,6 +398,7 @@ export default function DOMPlayer({
isInitialEventsLoaded,
playerState,
play,
debug,
]);
// Set player to the correct time based on focus
@ -440,7 +441,7 @@ export default function DOMPlayer({
}
abort();
};
}, []);
}, [abort]);
const isLoading = isInitialEventsLoaded === false && isSearchResultsFetching;
// TODO: Handle when ts is set to a value that's outside of this session

View file

@ -36,7 +36,7 @@ export default function Playbar({
queryKey: ['PlayBar', queriedConfig],
},
);
const events: any[] = data?.data ?? [];
const events: any[] = useMemo(() => data?.data ?? [], [data?.data]);
const markers = useMemo<PlaybarMarker[]>(() => {
return uniqBy(

View file

@ -320,10 +320,10 @@ export default function PodDetailsSidePanel({
}
return _where;
}, [
nodeName,
logSource,
doesPrimaryOrSortingKeysContainServiceExpression,
logServiceNames,
podName,
]);
const handleClose = React.useCallback(() => {

View file

@ -906,7 +906,13 @@ function ServicesDashboardPage() {
) {
onSubmit();
}
}, [service, sourceId]);
}, [
service,
sourceId,
appliedConfig.service,
appliedConfig.source,
onSubmit,
]);
return (
<Box p="sm">

View file

@ -114,7 +114,7 @@ export const SessionEventList = ({
});
const formatTime = useFormatTime();
const events = data?.data ?? [];
const events = React.useMemo(() => data?.data ?? [], [data?.data]);
const getRowWhere = useRowWhere({ meta: data?.meta, aliasMap });

View file

@ -306,7 +306,7 @@ export default function SessionsPage() {
if (sourceId !== appliedConfig.sessionSource) {
onSubmit();
}
}, [sourceId]);
}, [sourceId, appliedConfig.sessionSource, onSubmit]);
// FIXME: fix the url
const generateSearchUrl = useCallback(

View file

@ -840,7 +840,7 @@ function TeamNameSection() {
},
);
},
[refetchTeam, setTeamName, team?.name],
[refetchTeam, setTeamName],
);
return (
<Box id="team_name">

View file

@ -410,16 +410,20 @@ export default function TimelineChart({
}
};
useDrag(timelineRef, [], {
onDrag: e => {
setOffset(v =>
Math.min(
Math.max(v - e.movementX * (0.125 / scale), 0),
100 - 100 / scale,
),
);
},
});
const useDragOptions: Parameters<typeof useDrag>[1] = useMemo(
() => ({
onDrag: e => {
setOffset(v =>
Math.min(
Math.max(v - e.movementX * (0.125 / scale), 0),
100 - 100 / scale,
),
);
},
}),
[scale, setOffset],
);
useDrag(timelineRef, useDragOptions);
const [cursorXPerc, setCursorXPerc] = useState(0);

View file

@ -239,6 +239,10 @@ export default function ContextSubpanel({
originalLanguage,
newDateRange,
contextBy,
source.connection,
source.defaultTableSelectExpression,
source.from,
source.timestampValueExpression,
]);
return (

View file

@ -477,7 +477,7 @@ export default function EditTimeChartForm({
if (displayType !== DisplayType.Line) {
setValue('alert', undefined);
}
}, [displayType]);
}, [displayType, setValue]);
const showGeneratedSql = ['table', 'time', 'number'].includes(activeTab); // Whether to show the generated SQL preview
const showSampleEvents = tableSource?.kind !== SourceKind.Metric;

View file

@ -90,12 +90,15 @@ export function RowOverviewPanel({
const flattenedEventAttributes =
firstRow?.__hdx_event_attributes ?? EMPTY_OBJ;
const dataAttributes =
eventAttributesExpr &&
firstRow?.[eventAttributesExpr] &&
Object.keys(firstRow[eventAttributesExpr]).length > 0
? { [eventAttributesExpr]: firstRow[eventAttributesExpr] }
: {};
const dataAttributes = useMemo(
() =>
eventAttributesExpr &&
firstRow?.[eventAttributesExpr] &&
Object.keys(firstRow[eventAttributesExpr]).length > 0
? { [eventAttributesExpr]: firstRow[eventAttributesExpr] }
: {},
[eventAttributesExpr, firstRow],
);
const _generateSearchUrl = useCallback(
(query?: string, queryLanguage?: 'sql' | 'lucene') => {

View file

@ -151,27 +151,28 @@ export default function DBRowSidePanelHeader({
[bodyExpanded, mainContent],
);
const headerRef = useRef<HTMLDivElement>(null);
const [headerElement, setHeaderElement] = useState<HTMLDivElement | null>(
null,
);
const [headerHeight, setHeaderHeight] = useState(0);
useEffect(() => {
if (!headerRef.current) return;
const el = headerRef.current;
if (!headerElement) return;
const updateHeight = () => {
const newHeight = el.offsetHeight;
const newHeight = headerElement.offsetHeight;
setHeaderHeight(newHeight);
};
updateHeight();
// Set up a resize observer to detect height changes
const resizeObserver = new ResizeObserver(updateHeight);
resizeObserver.observe(el);
resizeObserver.observe(headerElement);
// Clean up the observer on component unmount
return () => {
resizeObserver.disconnect();
};
}, [headerRef.current, setHeaderHeight]);
}, [headerElement]);
const { userPreferences, setUserPreference } = useUserPreferences();
const { expandSidebarHeader } = userPreferences;
@ -221,7 +222,7 @@ export default function DBRowSidePanelHeader({
overflow: 'auto',
overflowWrap: 'break-word',
}}
ref={headerRef}
ref={setHeaderElement}
>
<Flex justify="space-between" mb="xs">
<Text size="xs">{mainContentHeader}</Text>

View file

@ -531,6 +531,7 @@ export const RawLogTable = memo(
expandedRows,
toggleRowExpansion,
showExpandButton,
aliasMap,
],
);

View file

@ -288,7 +288,7 @@ export function useEventsAroundFocus({
id: rowWhere(omit(cd, ['SpanAttributes', '__hdx_hidden'])),
};
});
}, [afterSpanData, beforeSpanData]);
}, [afterSpanData, beforeSpanData, meta, rowWhere, type]);
return {
rows,

View file

@ -157,7 +157,7 @@ export function MetricNameSelect({
});
}
return metricsFromQuery;
}, [gaugeMetrics, histogramMetrics, sumMetrics]);
}, [gaugeMetrics, histogramMetrics, sumMetrics, metricName, metricType]);
return (
<Select

View file

@ -225,23 +225,21 @@ export default function SQLInlineEditor({
// query search history
const [queryHistory, setQueryHistory] = useQueryHistory(queryHistoryType);
const onSelectSearchHistory = (
view: EditorView,
from: number,
to: number,
q: string,
) => {
// update history into search bar
view.dispatch({
changes: { from, to, insert: q },
});
// close history bar;
closeCompletion(view);
// update history order
setQueryHistory(q);
// execute search
if (onSubmit) onSubmit();
};
const onSelectSearchHistory = useCallback(
(view: EditorView, from: number, to: number, q: string) => {
// update history into search bar
view.dispatch({
changes: { from, to, insert: q },
});
// close history bar;
closeCompletion(view);
// update history order
setQueryHistory(q);
// execute search
if (onSubmit) onSubmit();
},
[onSubmit, setQueryHistory],
);
const createHistoryList = useMemo(() => {
return () => {
@ -264,7 +262,7 @@ export default function SQLInlineEditor({
}),
};
};
}, [queryHistory]);
}, [queryHistory, onSelectSearchHistory]);
const [isFocused, setIsFocused] = useState(false);
@ -304,7 +302,12 @@ export default function SQLInlineEditor({
),
});
},
[filteredFields, additionalSuggestions, queryHistory],
[
filteredFields,
additionalSuggestions,
createHistoryList,
disableKeywordAutocomplete,
],
);
useEffect(() => {

View file

@ -1133,29 +1133,32 @@ export function TableSourceForm({
}, [watch, kind, currentSourceId, sources, updateSource]);
const sourceFormSchema = sourceSchemaWithout({ id: true });
const handleError = (error: z.ZodError<TSourceUnion>) => {
const errors = error.errors;
for (const err of errors) {
const errorPath: string = err.path.join('.');
// TODO: HDX-1768 get rid of this type assertion if possible
setError(errorPath as any, { ...err });
}
notifications.show({
color: 'red',
message: (
<Stack>
<Text size="sm">
<b>Failed to create source</b>
</Text>
{errors.map((err, i) => (
<Text key={i} size="sm">
{err.message}
const handleError = useCallback(
(error: z.ZodError<TSourceUnion>) => {
const errors = error.errors;
for (const err of errors) {
const errorPath: string = err.path.join('.');
// TODO: HDX-1768 get rid of this type assertion if possible
setError(errorPath as any, { ...err });
}
notifications.show({
color: 'red',
message: (
<Stack>
<Text size="sm">
<b>Failed to create source</b>
</Text>
))}
</Stack>
),
});
};
{errors.map((err, i) => (
<Text key={i} size="sm">
{err.message}
</Text>
))}
</Stack>
),
});
},
[setError],
);
const _onCreate = useCallback(() => {
clearErrors();
@ -1215,11 +1218,12 @@ export function TableSourceForm({
);
})();
}, [
clearErrors,
handleError,
sourceFormSchema,
handleSubmit,
createSource,
onCreate,
kind,
formState,
sources,
updateSource,
]);
@ -1252,7 +1256,14 @@ export function TableSourceForm({
},
);
})();
}, [handleSubmit, updateSource, onSave]);
}, [
handleSubmit,
updateSource,
onSave,
clearErrors,
handleError,
sourceFormSchema,
]);
const databaseName = watch(`from.databaseName`, DEFAULT_DATABASE);
const connectionId = watch(`connection`);

View file

@ -173,7 +173,7 @@ export function useAutoCompleteOptions(
});
});
return output;
}, [fieldCompleteOptions, keyVals, searchField]);
}, [fieldCompleteOptions, keyVals, searchField, formatter]);
// combine all autocomplete options
return useMemo(() => {

View file

@ -237,7 +237,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
window.removeEventListener('customStorage', handleCustomStorageChange);
window.removeEventListener('storage', handleStorageChange);
};
}, []);
}, [instanceId, key]);
// Fetch the value on client-side to avoid SSR issues
useEffect(() => {
@ -314,15 +314,18 @@ export function useQueryHistory<T>(type: string | undefined) {
export function useIntersectionObserver(onIntersect: () => void) {
const observer = useRef<IntersectionObserver | null>(null);
const observerRef = useCallback((node: Element | null) => {
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) {
onIntersect();
}
});
if (node) observer.current.observe(node);
}, []);
const observerRef = useCallback(
(node: Element | null) => {
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) {
onIntersect();
}
});
if (node) observer.current.observe(node);
},
[onIntersect],
);
return { observerRef };
}
@ -509,7 +512,6 @@ export const usePrevious = <T>(value: T): T | undefined => {
// From https://javascript.plainenglish.io/how-to-make-a-simple-custom-usedrag-react-hook-6b606d45d353
export const useDrag = (
ref: MutableRefObject<HTMLDivElement | null>,
deps = [],
options: {
onDrag?: (e: PointerEvent) => any;
onPointerDown?: (e: PointerEvent) => any;
@ -559,9 +561,9 @@ export const useDrag = (
element.removeEventListener('pointermove', handlePointerMove);
};
}
return () => {};
}, [...deps, isDragging]);
// disable dependency array as this doesn't fit nicely with react
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return { isDragging };
};