Skip to content

Commit e11b313

Browse files
authored
fix: add react-hooks eslint to catch pitfalls (#1661)
Had a couple of files with large fixes, I tested AutocompleteInput.tsx and SessionSubpanel.tsx quite a bit. Tested most others as well. I didn't act on some due to either a) correct usage or b) they are stable and I don't want to alter it. In both those cases, I added `eslint-disable-next-line` before each relevant line References HDX-2955
1 parent b6c34b1 commit e11b313

34 files changed

Lines changed: 232 additions & 225 deletions

‎.changeset/rotten-kings-rhyme.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/app": patch
3+
---
4+
5+
fix: add react-hooks-eslint-plugin and fix issues across app

‎packages/app/eslint.config.mjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export default [
5050
rules: {
5151
...nextPlugin.configs.recommended.rules,
5252
...nextPlugin.configs['core-web-vitals'].rules,
53+
...reactHooksPlugin.configs.recommended.rules,
54+
'react-hooks/set-state-in-effect': 'warn',
5355
'@typescript-eslint/ban-ts-comment': 'warn',
5456
'@typescript-eslint/no-empty-function': 'warn',
5557
'@typescript-eslint/no-explicit-any': 'off',

‎packages/app/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@
137137
"eslint-config-next": "^16.0.10",
138138
"eslint-plugin-playwright": "^2.4.0",
139139
"eslint-plugin-react-hook-form": "^0.3.1",
140+
"eslint-plugin-react-hooks": "^7.0.1",
140141
"eslint-plugin-storybook": "10.1.4",
141142
"identity-obj-proxy": "^3.0.0",
142143
"jest": "^30.2.0",

‎packages/app/src/AutocompleteInput.tsx‎

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useMemo, useRef, useState } from 'react';
1+
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
22
import Fuse from 'fuse.js';
33
import { Popover, Textarea, UnstyledButton } from '@mantine/core';
44

@@ -46,9 +46,19 @@ export default function AutocompleteInput({
4646
}) {
4747
const suggestionsLimit = 10;
4848

49-
const [isSearchInputFocused, setIsSearchInputFocused] = useState(false);
49+
const [isSearchInputFocused, _setIsSearchInputFocused] = useState(false);
5050
const [isInputDropdownOpen, setIsInputDropdownOpen] = useState(false);
51-
const [showSearchHistory, setShowSearchHistory] = useState(false);
51+
const setIsSearchInputFocused = useCallback(
52+
(state: boolean) => {
53+
_setIsSearchInputFocused(state);
54+
setIsInputDropdownOpen(state);
55+
},
56+
[_setIsSearchInputFocused],
57+
);
58+
const [rightSectionWidth, setRightSectionWidth] = useState<number | 'auto'>(
59+
'auto',
60+
);
61+
const [inputWidth, setInputWidth] = useState<number>(720);
5262

5363
const [selectedAutocompleteIndex, setSelectedAutocompleteIndex] =
5464
useState(-1);
@@ -67,25 +77,11 @@ export default function AutocompleteInput({
6777
});
6878
}, [queryHistory, queryHistoryType]);
6979

70-
useEffect(() => {
71-
if (isSearchInputFocused) {
72-
setIsInputDropdownOpen(true);
73-
}
74-
}, [isSearchInputFocused]);
75-
76-
useEffect(() => {
77-
// only show search history when: 1.no input, 2.has search type, 3.has history list
78-
if (
79-
value != null &&
80-
value.length === 0 &&
81-
queryHistoryList.length > 0 &&
82-
queryHistoryType
83-
) {
84-
setShowSearchHistory(true);
85-
} else {
86-
setShowSearchHistory(false);
87-
}
88-
}, [value, queryHistoryType, queryHistoryList]);
80+
const showSearchHistory =
81+
value != null &&
82+
value.length === 0 &&
83+
queryHistoryList.length > 0 &&
84+
queryHistoryType;
8985

9086
const fuse = useMemo(
9187
() =>
@@ -128,6 +124,14 @@ export default function AutocompleteInput({
128124
inputRef.current?.focus();
129125
};
130126
const ref = useRef<HTMLDivElement>(null);
127+
useLayoutEffect(() => {
128+
if (ref.current) {
129+
setRightSectionWidth(ref.current.clientWidth);
130+
}
131+
if (inputRef.current) {
132+
setInputWidth(inputRef.current.clientWidth);
133+
}
134+
}, [language, onLanguageChange, inputRef]);
131135

132136
return (
133137
<Popover
@@ -141,10 +145,7 @@ export default function AutocompleteInput({
141145
closeOnEscape
142146
styles={{
143147
dropdown: {
144-
maxWidth:
145-
(inputRef.current?.clientWidth || 0) > 300
146-
? inputRef.current?.clientWidth
147-
: 720,
148+
maxWidth: inputWidth > 300 ? inputWidth : 720,
148149
width: '100%',
149150
zIndex,
150151
},
@@ -242,7 +243,7 @@ export default function AutocompleteInput({
242243
}
243244
}
244245
}}
245-
rightSectionWidth={ref.current?.clientWidth ?? 'auto'}
246+
rightSectionWidth={rightSectionWidth}
246247
rightSection={
247248
language != null && onLanguageChange != null ? (
248249
<div ref={ref}>

‎packages/app/src/DBChartPage.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ function AIAssistant({
164164
</Group>
165165
<Collapse in={opened}>
166166
{opened && (
167+
// eslint-disable-next-line react-hooks/refs
167168
<form onSubmit={handleSubmit(onSubmit)}>
168169
<Group mb="md">
169170
<SourceSelectControlled

‎packages/app/src/DBSearchPage.tsx‎

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -764,19 +764,16 @@ export function useDefaultOrderBy(sourceID: string | undefined | null) {
764764
const { data: source } = useSource({ id: sourceID });
765765
const { data: tableMetadata } = useTableMetadata(tcFromSource(source));
766766

767-
// If no source, return undefined so that the orderBy is not set incorrectly
768-
if (!source) return undefined;
769-
770767
// When source changes, make sure select and orderby fields are set to default
771-
return useMemo(
772-
() =>
773-
optimizeDefaultOrderBy(
774-
source?.timestampValueExpression ?? '',
775-
source?.displayedTimestampValueExpression,
776-
tableMetadata?.sorting_key,
777-
),
778-
[source, tableMetadata],
779-
);
768+
return useMemo(() => {
769+
// If no source, return undefined so that the orderBy is not set incorrectly
770+
if (!source) return undefined;
771+
return optimizeDefaultOrderBy(
772+
source?.timestampValueExpression ?? '',
773+
source?.displayedTimestampValueExpression,
774+
tableMetadata?.sorting_key,
775+
);
776+
}, [source, tableMetadata]);
780777
}
781778

782779
// This is outside as it needs to be a stable reference

‎packages/app/src/DOMPlayer.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ export default function DOMPlayer({
264264
);
265265
}
266266

267+
// eslint-disable-next-line react-hooks/immutability
267268
updatePlayerTimeRafRef.current = requestAnimationFrame(updatePlayerTime);
268269
}, []);
269270

‎packages/app/src/LandingHeader.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default function LandingHeader({
1212
activeKey: string;
1313
fixed?: boolean;
1414
}) {
15-
const Wordmark = useWordmark();
15+
const wordmark = useWordmark();
1616
const { data: me } = api.useMe();
1717
const isLoggedIn = Boolean(me);
1818

@@ -36,7 +36,7 @@ export default function LandingHeader({
3636
<Container fluid px="xl" py="md">
3737
<Group justify="space-between" align="center">
3838
<Link href="/" style={{ textDecoration: 'none' }}>
39-
<Wordmark />
39+
{wordmark}
4040
</Link>
4141

4242
<Burger

‎packages/app/src/OnboardingChecklist.tsx‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
} from '@tabler/icons-react';
2121

2222
import { useQueriedChartConfig } from './hooks/useChartConfig';
23-
import { useLogomark } from './theme/ThemeProvider';
2423
import api from './api';
2524
import { useConnections } from './connection';
2625
import { useSources } from './source';
@@ -36,12 +35,12 @@ interface OnboardingStep {
3635
onClick?: () => void;
3736
}
3837

38+
const NOW = Date.now();
3939
const OnboardingChecklist = ({
4040
onAddDataClick,
4141
}: {
4242
onAddDataClick?: () => void;
4343
}) => {
44-
const Logomark = useLogomark();
4544
const [isCollapsed, setIsCollapsed] = useLocalStorage(
4645
'onboardingChecklistCollapsed',
4746
false,
@@ -55,7 +54,7 @@ const OnboardingChecklist = ({
5554
// Check if team is new (less than 3 days old)
5655
const isNewTeam = useMemo(() => {
5756
if (!team?.createdAt) return false;
58-
const threeDaysAgo = new Date(Date.now() - 1000 * 60 * 60 * 24 * 3);
57+
const threeDaysAgo = new Date(NOW - 1000 * 60 * 60 * 24 * 3);
5958
return new Date(team.createdAt) > threeDaysAgo;
6059
}, [team]);
6160

0 commit comments

Comments
 (0)