Skip to content

Commit 468eb92

Browse files
chore: update watch uses to useWatch (#1516)
Fixes: HDX-3101 Co-authored-by: Brandon Pereira <[email protected]>
1 parent 6537884 commit 468eb92

29 files changed

+688
-429
lines changed

.changeset/yellow-files-deny.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+
Update some forms to work better with React 19

packages/app/eslint.config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import prettierConfig from 'eslint-config-prettier';
88
import simpleImportSort from 'eslint-plugin-simple-import-sort';
99
import prettierPlugin from 'eslint-plugin-prettier/recommended';
1010
import playwrightPlugin from 'eslint-plugin-playwright';
11+
import reactHookFormPlugin from 'eslint-plugin-react-hook-form';
12+
import { fixupPluginRules } from '@eslint/compat';
1113

1214
export default [
1315
js.configs.recommended,
@@ -39,6 +41,7 @@ export default [
3941
react: reactPlugin,
4042
'react-hooks': reactHooksPlugin,
4143
'simple-import-sort': simpleImportSort,
44+
'react-hook-form': fixupPluginRules(reactHookFormPlugin), // not compatible with eslint 9 yet
4245
},
4346
rules: {
4447
...nextPlugin.configs.recommended.rules,
@@ -81,6 +84,7 @@ export default [
8184
],
8285
'react-hooks/exhaustive-deps': 'error',
8386
'no-console': ['error', { allow: ['warn', 'error'] }],
87+
'react-hook-form/no-use-watch': 'error',
8488
},
8589
languageOptions: {
8690
parser: tseslint.parser,

packages/app/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
},
105105
"devDependencies": {
106106
"@chromatic-com/storybook": "^4.1.3",
107+
"@eslint/compat": "^2.0.0",
107108
"@hookform/devtools": "^4.3.1",
108109
"@jedmao/location": "^3.0.0",
109110
"@playwright/test": "^1.57.0",
@@ -135,6 +136,7 @@
135136
"@types/sqlstring": "^2.3.2",
136137
"eslint-config-next": "^16.0.10",
137138
"eslint-plugin-playwright": "^2.4.0",
139+
"eslint-plugin-react-hook-form": "^0.3.1",
138140
"eslint-plugin-storybook": "10.1.4",
139141
"identity-obj-proxy": "^3.0.0",
140142
"jest": "^30.2.0",

packages/app/src/AuthPage.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import Link from 'next/link';
55
import { useRouter } from 'next/router';
66
import { NextSeo } from 'next-seo';
77
import cx from 'classnames';
8-
import { SubmitHandler, useForm } from 'react-hook-form';
8+
import { SubmitHandler, useForm, useWatch } from 'react-hook-form';
99
import {
1010
Button,
1111
Notification,
@@ -45,7 +45,7 @@ export default function AuthPage({ action }: { action: 'register' | 'login' }) {
4545
handleSubmit,
4646
formState: { errors, isSubmitting },
4747
setError,
48-
watch,
48+
control,
4949
} = useForm<FormData>({
5050
reValidateMode: 'onSubmit',
5151
});
@@ -67,8 +67,16 @@ export default function AuthPage({ action }: { action: 'register' | 'login' }) {
6767
}
6868
}, [installation, isRegister, router]);
6969

70-
const currentPassword = watch('password', '');
71-
const confirmPassword = watch('confirmPassword', '');
70+
const currentPassword = useWatch({
71+
control,
72+
name: 'password',
73+
defaultValue: '',
74+
});
75+
const confirmPassword = useWatch({
76+
control,
77+
name: 'confirmPassword',
78+
defaultValue: '',
79+
});
7280

7381
const confirmPass = () => {
7482
return currentPassword === confirmPassword;

packages/app/src/ClickhousePage.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { useMemo, useState } from 'react';
1+
import { useEffect, useMemo, useState } from 'react';
22
import dynamic from 'next/dynamic';
33
import {
44
parseAsFloat,
55
parseAsStringEnum,
66
useQueryState,
77
useQueryStates,
88
} from 'nuqs';
9-
import { useForm } from 'react-hook-form';
9+
import { useForm, useWatch } from 'react-hook-form';
1010
import { sql } from '@codemirror/lang-sql';
1111
import { format as formatSql } from '@hyperdx/common-utils/dist/sqlFormatter';
1212
import { DisplayType } from '@hyperdx/common-utils/dist/types';
@@ -436,17 +436,19 @@ function ClickhousePage() {
436436

437437
const connection = _connection ?? connections?.[0]?.id ?? '';
438438

439-
const { control, watch } = useForm({
439+
const { control } = useForm({
440440
values: {
441441
connection,
442442
},
443443
});
444444

445-
watch((data, { name, type }) => {
446-
if (name === 'connection' && type === 'change') {
447-
setConnection(data.connection ?? null);
445+
const watchedConnection = useWatch({ control, name: 'connection' });
446+
447+
useEffect(() => {
448+
if (watchedConnection !== connection) {
449+
setConnection(watchedConnection ?? null);
448450
}
449-
});
451+
}, [watchedConnection, connection, setConnection]);
450452
const DEFAULT_INTERVAL = 'Past 1h';
451453
const [displayedTimeInputValue, setDisplayedTimeInputValue] =
452454
useState(DEFAULT_INTERVAL);

packages/app/src/DBChartPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useCallback, useRef, useState } from 'react';
22
import dynamic from 'next/dynamic';
33
import Head from 'next/head';
44
import { parseAsJson, parseAsStringEnum, useQueryState } from 'nuqs';
5-
import { useForm } from 'react-hook-form';
5+
import { useForm, useWatch } from 'react-hook-form';
66
import { useHotkeys } from 'react-hotkeys-hook';
77
import { SavedChartConfig, SourceKind } from '@hyperdx/common-utils/dist/types';
88
import {
@@ -57,7 +57,7 @@ function AIAssistant({
5757
'ai-assistant-alert-dismissed',
5858
false,
5959
);
60-
const { control, watch, setValue, handleSubmit } = useForm<{
60+
const { control, setValue, handleSubmit } = useForm<{
6161
text: string;
6262
source: string;
6363
}>({

packages/app/src/DBDashboardImportPage.tsx

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import dynamic from 'next/dynamic';
33
import Head from 'next/head';
44
import { useRouter } from 'next/router';
55
import { filter } from 'lodash';
6-
import { Controller, useForm } from 'react-hook-form';
6+
import { Controller, useForm, useWatch } from 'react-hook-form';
77
import { StringParam, useQueryParam } from 'use-query-params';
88
import { z } from 'zod';
99
import { zodResolver } from '@hookform/resolvers/zod';
@@ -191,7 +191,7 @@ function Mapping({ input }: { input: Input }) {
191191
const { data: sources } = useSources();
192192
const [dashboardId] = useQueryParam('dashboardId', StringParam);
193193

194-
const { handleSubmit, getFieldState, control, setValue, watch } =
194+
const { handleSubmit, getFieldState, control, setValue } =
195195
useForm<SourceResolutionFormValues>({
196196
resolver: zodResolver(SourceResolutionForm),
197197
defaultValues: {
@@ -226,16 +226,24 @@ function Mapping({ input }: { input: Input }) {
226226
}, [setValue, sources, input]);
227227

228228
const isUpdatingRef = useRef(false);
229-
watch((a, { name }) => {
229+
const sourceMappings = useWatch({ control, name: 'sourceMappings' });
230+
const prevSourceMappingsRef = useRef(sourceMappings);
231+
232+
useEffect(() => {
230233
if (isUpdatingRef.current) return;
231-
if (!a.sourceMappings || !input.tiles) return;
232-
const [, inputIdx] = name?.split('.') || [];
233-
if (!inputIdx) return;
234+
if (!sourceMappings || !input.tiles) return;
235+
236+
// Find which mapping changed
237+
const changedIdx = sourceMappings.findIndex(
238+
(mapping, idx) => mapping !== prevSourceMappingsRef.current?.[idx],
239+
);
240+
if (changedIdx === -1) return;
234241

235-
const idx = Number(inputIdx);
236-
const inputTile = input.tiles[idx];
242+
prevSourceMappingsRef.current = sourceMappings;
243+
244+
const inputTile = input.tiles[changedIdx];
237245
if (!inputTile) return;
238-
const sourceId = a.sourceMappings[idx] ?? '';
246+
const sourceId = sourceMappings[changedIdx] ?? '';
239247
const keysForTilesWithMatchingSource = input.tiles
240248
.map((tile, index) => ({ ...tile, index }))
241249
.filter(tile => tile.config.source === inputTile.config.source)
@@ -263,7 +271,7 @@ function Mapping({ input }: { input: Input }) {
263271
}
264272

265273
isUpdatingRef.current = false;
266-
});
274+
}, [sourceMappings, input.tiles, input.filters, getFieldState, setValue]);
267275

268276
const createDashboard = useCreateDashboard();
269277
const updateDashboard = useUpdateDashboard();

packages/app/src/DBDashboardPage.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import produce from 'immer';
1414
import { parseAsString, useQueryState } from 'nuqs';
1515
import { ErrorBoundary } from 'react-error-boundary';
1616
import RGL, { WidthProvider } from 'react-grid-layout';
17-
import { Controller, useForm } from 'react-hook-form';
17+
import { Controller, useForm, useWatch } from 'react-hook-form';
1818
import { TableConnection } from '@hyperdx/common-utils/dist/core/metadata';
1919
import { convertToDashboardTemplate } from '@hyperdx/common-utils/dist/core/utils';
2020
import {
@@ -644,7 +644,7 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
644644

645645
const [isLive, setIsLive] = useState(false);
646646

647-
const { control, watch, setValue, handleSubmit } = useForm<{
647+
const { control, setValue, handleSubmit } = useForm<{
648648
granularity: SQLInterval | 'auto';
649649
where: SearchCondition;
650650
whereLanguage: SearchConditionLanguage;
@@ -660,11 +660,14 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
660660
whereLanguage: (whereLanguage as SearchConditionLanguage) ?? 'lucene',
661661
},
662662
});
663-
watch((data, { name, type }) => {
664-
if (name === 'granularity' && type === 'change') {
665-
setGranularity(data.granularity as SQLInterval);
663+
664+
const watchedGranularity = useWatch({ control, name: 'granularity' });
665+
666+
useEffect(() => {
667+
if (watchedGranularity && watchedGranularity !== granularity) {
668+
setGranularity(watchedGranularity as SQLInterval);
666669
}
667-
});
670+
}, [watchedGranularity, granularity, setGranularity]);
668671

669672
const [displayedTimeInputValue, setDisplayedTimeInputValue] =
670673
useState('Past 1h');

packages/app/src/DBSearchPage.tsx

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -843,7 +843,6 @@ function DBSearchPage() {
843843

844844
const {
845845
control,
846-
watch,
847846
setValue,
848847
reset,
849848
handleSubmit,
@@ -1004,31 +1003,31 @@ function DBSearchPage() {
10041003
onFilterChange: handleSetFilters,
10051004
});
10061005

1006+
const watchedSource = useWatch({ control, name: 'source' });
1007+
const prevSourceRef = useRef(watchedSource);
1008+
10071009
useEffect(() => {
1008-
const { unsubscribe } = watch((data, { name, type }) => {
1009-
// If the user changes the source dropdown, reset the select and orderby fields
1010-
// to match the new source selected
1011-
if (name === 'source' && type === 'change') {
1012-
const newInputSourceObj = inputSourceObjs?.find(
1013-
s => s.id === data.source,
1010+
// If the user changes the source dropdown, reset the select and orderby fields
1011+
// to match the new source selected
1012+
if (watchedSource !== prevSourceRef.current) {
1013+
prevSourceRef.current = watchedSource;
1014+
const newInputSourceObj = inputSourceObjs?.find(
1015+
s => s.id === watchedSource,
1016+
);
1017+
if (newInputSourceObj != null) {
1018+
// Save the selected source ID to localStorage
1019+
setLastSelectedSourceId(newInputSourceObj.id);
1020+
1021+
setValue(
1022+
'select',
1023+
newInputSourceObj?.defaultTableSelectExpression ?? '',
10141024
);
1015-
if (newInputSourceObj != null) {
1016-
// Save the selected source ID to localStorage
1017-
setLastSelectedSourceId(newInputSourceObj.id);
1018-
1019-
setValue(
1020-
'select',
1021-
newInputSourceObj?.defaultTableSelectExpression ?? '',
1022-
);
1023-
// Clear all search filters
1024-
searchFilters.clearAllFilters();
1025-
}
1025+
// Clear all search filters
1026+
searchFilters.clearAllFilters();
10261027
}
1027-
});
1028-
return () => unsubscribe();
1028+
}
10291029
}, [
1030-
watch,
1031-
inputSourceObj,
1030+
watchedSource,
10321031
setValue,
10331032
inputSourceObjs,
10341033
searchFilters,

packages/app/src/DBSearchPageAlertModal.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import router from 'next/router';
3-
import { useForm } from 'react-hook-form';
3+
import { useForm, useWatch } from 'react-hook-form';
44
import { NativeSelect, NumberInput } from 'react-hook-form-mantine';
55
import { z } from 'zod';
66
import { zodResolver } from '@hookform/resolvers/zod';
@@ -92,7 +92,7 @@ const AlertForm = ({
9292
}) => {
9393
const { data: source } = useSource({ id: sourceId });
9494

95-
const { control, handleSubmit, watch } = useForm<Alert>({
95+
const { control, handleSubmit } = useForm<Alert>({
9696
defaultValues: defaultValues || {
9797
interval: '5m',
9898
threshold: 1,
@@ -106,8 +106,13 @@ const AlertForm = ({
106106
resolver: zodResolver(SavedSearchAlertFormSchema),
107107
});
108108

109-
const groupBy = watch('groupBy');
110-
const thresholdType = watch('thresholdType');
109+
const groupBy = useWatch({ control, name: 'groupBy' });
110+
const thresholdType = useWatch({ control, name: 'thresholdType' });
111+
const channelType = useWatch({ control, name: 'channel.type' });
112+
const interval = useWatch({ control, name: 'interval' });
113+
const groupByValue = useWatch({ control, name: 'groupBy' });
114+
const threshold = useWatch({ control, name: 'threshold' });
115+
const thresholdTypeValue = useWatch({ control, name: 'thresholdType' });
111116

112117
return (
113118
<form onSubmit={handleSubmit(onSubmit)}>
@@ -168,7 +173,7 @@ const AlertForm = ({
168173
<Text size="xxs" opacity={0.5} mb={4}>
169174
Send to
170175
</Text>
171-
<AlertChannelForm control={control} type={watch('channel.type')} />
176+
<AlertChannelForm control={control} type={channelType} />
172177
</Paper>
173178
{groupBy && thresholdType === AlertThresholdType.BELOW && (
174179
<MantineAlert
@@ -197,10 +202,10 @@ const AlertForm = ({
197202
where={where}
198203
whereLanguage={whereLanguage}
199204
select={select}
200-
interval={watch('interval')}
201-
groupBy={watch('groupBy')}
202-
threshold={watch('threshold')}
203-
thresholdType={watch('thresholdType')}
205+
interval={interval}
206+
groupBy={groupByValue}
207+
threshold={threshold}
208+
thresholdType={thresholdTypeValue}
204209
/>
205210
)}
206211
</Accordion.Panel>

0 commit comments

Comments
 (0)