diff --git a/site/package.json b/site/package.json index 70e2d164a0a41..430f088c4e983 100644 --- a/site/package.json +++ b/site/package.json @@ -158,7 +158,6 @@ "jest-runner-eslint": "2.1.0", "jest-websocket-mock": "2.5.0", "jest_workaround": "0.1.14", - "just-debounce-it": "3.2.0", "msw": "1.3.0", "prettier": "3.0.0", "protobufjs": "7.2.4", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 232ae01f3b551..f4b4bbfba94a8 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -386,9 +386,6 @@ devDependencies: jest_workaround: specifier: 0.1.14 version: 0.1.14(@swc/core@1.3.38)(@swc/jest@0.2.24) - just-debounce-it: - specifier: 3.2.0 - version: 3.2.0 msw: specifier: 1.3.0 version: 1.3.0(typescript@5.1.6) @@ -10523,10 +10520,6 @@ packages: object.values: 1.1.6 dev: true - /just-debounce-it@3.2.0: - resolution: {integrity: sha512-WXzwLL0745uNuedrCsCs3rpmfD6DBaf7uuVwaq98/8dafURfgQaBsSpjiPp5+CW6Vjltwy9cOGI6qE71b3T8iQ==} - dev: true - /keyv@4.5.3: resolution: {integrity: sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==} dependencies: diff --git a/site/src/components/UserAutocomplete/UserAutocomplete.tsx b/site/src/components/UserAutocomplete/UserAutocomplete.tsx index b144c13bc072d..f76b61e60529e 100644 --- a/site/src/components/UserAutocomplete/UserAutocomplete.tsx +++ b/site/src/components/UserAutocomplete/UserAutocomplete.tsx @@ -6,10 +6,17 @@ import { useMachine } from "@xstate/react"; import { User } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/AvatarData/AvatarData"; -import debounce from "just-debounce-it"; -import { ChangeEvent, ComponentProps, FC, useEffect, useState } from "react"; +import { + ChangeEvent, + ComponentProps, + FC, + useEffect, + useRef, + useState, +} from "react"; import { searchUserMachine } from "xServices/users/searchUserXService"; import Box from "@mui/material/Box"; +import { useDebouncedFunction } from "hooks/debounce"; export type UserAutocompleteProps = { value: User | null; @@ -31,16 +38,23 @@ export const UserAutocomplete: FC = ({ const [searchState, sendSearch] = useMachine(searchUserMachine); const { searchResults } = searchState.context; - // seed list of options on the first page load if a user pases in a value - // since some organizations have long lists of users, we do not load all options on page load. + // Seed list of options on the first page load if a user passes in a value. + // Since some organizations have long lists of users, we do not want to load + // all options on page load. + const onMountRef = useRef(value); useEffect(() => { - if (value) { - sendSearch("SEARCH", { query: value.email }); + const mountValue = onMountRef.current; + if (mountValue) { + sendSearch("SEARCH", { query: mountValue.email }); } - // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO look into this - }, []); - const handleFilterChange = debounce( + // This isn't in XState's docs, but its source code guarantees that the + // memory reference of sendSearch will stay stable across renders. This + // useEffect call will behave like an on-mount effect and will not ever need + // to resynchronize + }, [sendSearch]); + + const { debounced: debouncedOnChange } = useDebouncedFunction( (event: ChangeEvent) => { sendSearch("SEARCH", { query: event.target.value }); }, @@ -93,7 +107,7 @@ export const UserAutocomplete: FC = ({ className={styles.textField} InputProps={{ ...params.InputProps, - onChange: handleFilterChange, + onChange: debouncedOnChange, startAdornment: value && ( {value.username} diff --git a/site/src/components/UserOrGroupAutocomplete/UserOrGroupAutocomplete.tsx b/site/src/components/UserOrGroupAutocomplete/UserOrGroupAutocomplete.tsx index 8d400ecac46a5..51c31a8320c5d 100644 --- a/site/src/components/UserOrGroupAutocomplete/UserOrGroupAutocomplete.tsx +++ b/site/src/components/UserOrGroupAutocomplete/UserOrGroupAutocomplete.tsx @@ -5,11 +5,11 @@ import Autocomplete from "@mui/material/Autocomplete"; import { useMachine } from "@xstate/react"; import { Group, User } from "api/typesGenerated"; import { AvatarData } from "components/AvatarData/AvatarData"; -import debounce from "just-debounce-it"; import { ChangeEvent, useState } from "react"; import { getGroupSubtitle } from "utils/groups"; import { searchUsersAndGroupsMachine } from "xServices/template/searchUsersAndGroupsXService"; import Box from "@mui/material/Box"; +import { useDebouncedFunction } from "hooks/debounce"; export type UserOrGroupAutocompleteValue = User | Group | null; @@ -44,7 +44,7 @@ export const UserOrGroupAutocomplete: React.FC< return !excludeIds.includes(result.id); }); - const handleFilterChange = debounce( + const { debounced: handleFilterChange } = useDebouncedFunction( (event: ChangeEvent) => { sendSearch("SEARCH", { query: event.target.value }); },