Skip to content

Commit b0bf803

Browse files
committed
Use properly memoized function with throttling not debouncing
1 parent a33d976 commit b0bf803

File tree

4 files changed

+65
-42
lines changed

4 files changed

+65
-42
lines changed

packages/cli-kit/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@
170170
"@types/diff": "^5.2.3",
171171
"@types/fs-extra": "9.0.13",
172172
"@types/gradient-string": "^1.1.2",
173-
"@types/lodash": "4.17.14",
173+
"@types/lodash": "4.17.19",
174174
"@types/react": "18.2.0",
175175
"@types/semver": "^7.5.2",
176176
"@types/which": "3.0.4",

packages/cli-kit/src/private/node/ui/components/AutocompletePrompt.tsx

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {InfoTableProps} from './Prompts/InfoTable.js'
33
import {TextInput} from './TextInput.js'
44
import {InfoMessageProps} from './Prompts/InfoMessage.js'
55
import {Message, PromptLayout} from './Prompts/PromptLayout.js'
6-
import {debounce} from '../../../../public/common/function.js'
6+
import {throttle} from '../../../../public/common/function.js'
77
import {AbortSignal} from '../../../../public/node/abort.js'
88
import usePrompt, {PromptState} from '../hooks/use-prompt.js'
99
import React, {ReactElement, useCallback, useEffect, useRef, useState} from 'react'
@@ -84,40 +84,40 @@ function AutocompletePrompt<T>({
8484
const searchTermRef = useRef('')
8585
searchTermRef.current = searchTerm
8686

87-
// disable exhaustive-deps because we want to memoize the debounce function itself
88-
// eslint-disable-next-line react-hooks/exhaustive-deps
89-
const debounceSearch = useCallback(
90-
debounce(
91-
(term: string) => {
92-
setLoadingWhenSlow.current = setTimeout(() => {
93-
setPromptState(PromptState.Loading)
94-
}, 100)
95-
paginatedSearch(term)
96-
.then((result) => {
97-
// while we were waiting for the promise to resolve, the user
98-
// has emptied the search term, so we want to show the default
99-
// choices instead
100-
if (searchTermRef.current.length === 0) {
101-
setSearchResults(choices)
102-
setHasMorePages(initialHasMorePages)
103-
} else {
104-
setSearchResults(result.data)
105-
setHasMorePages(result.meta?.hasNextPage ?? false)
106-
}
87+
// useMemo ensures debounceSearch is not recreated on every render
88+
const debounceSearch = React.useMemo(
89+
() =>
90+
throttle(
91+
(term: string) => {
92+
setLoadingWhenSlow.current = setTimeout(() => {
93+
setPromptState(PromptState.Loading)
94+
}, 100)
95+
paginatedSearch(term)
96+
.then((result) => {
97+
// while we were waiting for the promise to resolve, the user
98+
// has emptied the search term, so we want to show the default
99+
// choices instead
100+
if (searchTermRef.current.length === 0) {
101+
setSearchResults(choices)
102+
setHasMorePages(initialHasMorePages)
103+
} else {
104+
setSearchResults(result.data)
105+
setHasMorePages(result.meta?.hasNextPage ?? false)
106+
}
107107

108-
setPromptState(PromptState.Idle)
109-
})
110-
.catch(() => {
111-
setPromptState(PromptState.Error)
112-
})
113-
.finally(() => {
114-
clearTimeout(setLoadingWhenSlow.current)
115-
})
116-
},
117-
300,
118-
{leading: true},
119-
),
120-
[initialHasMorePages, choices, paginatedSearch, searchResults],
108+
setPromptState(PromptState.Idle)
109+
})
110+
.catch(() => {
111+
setPromptState(PromptState.Error)
112+
})
113+
.finally(() => {
114+
clearTimeout(setLoadingWhenSlow.current)
115+
})
116+
},
117+
400,
118+
{leading: true, trailing: true},
119+
),
120+
[paginatedSearch],
121121
)
122122

123123
return (

packages/cli-kit/src/public/common/function.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import lodashMemoize from 'lodash/memoize.js'
22
import lodashDebounce from 'lodash/debounce.js'
3-
import type {DebouncedFunc, DebounceSettings} from 'lodash'
3+
import lodashThrottle from 'lodash/throttle.js'
4+
import type {DebouncedFunc, DebounceSettings, ThrottleSettings} from 'lodash'
45

56
/**
67
* Creates a function that memoizes the result of func. If resolver is provided it determines the cache key for
@@ -27,7 +28,7 @@ export function memoize<T extends (...args: any) => any>(func: T, resolver?: (..
2728
* Note: If leading and trailing options are true, func is invoked on the trailing edge of the timeout only
2829
* if the the debounced function is invoked more than once during the wait timeout.
2930
*
30-
* See David Corbachos article for details over the differences between _.debounce and _.throttle.
31+
* See David Corbacho's article for details over the differences between _.debounce and _.throttle.
3132
*
3233
* @param func - The function to debounce.
3334
* @param wait - The number of milliseconds to delay.
@@ -42,3 +43,25 @@ export function debounce<T extends (...args: any) => any>(
4243
): DebouncedFunc<T> {
4344
return lodashDebounce(func, wait, options)
4445
}
46+
47+
/**
48+
* Creates a throttled function that only invokes func at most once per every wait milliseconds.
49+
* The throttled function comes with a cancel method to cancel delayed invocations and a flush method to immediately invoke them.
50+
* Provide an options object to indicate whether func should be invoked on the leading and/or trailing edge of the wait timeout.
51+
* Subsequent calls to the throttled function return the result of the last func invocation.
52+
*
53+
* See David Corbacho's article for details over the differences between _.debounce and _.throttle.
54+
*
55+
* @param func - The function to throttle.
56+
* @param wait - The number of milliseconds to throttle invocations to.
57+
* @param options - The options object.
58+
* @returns Returns the new throttled function.
59+
*/
60+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
61+
export function throttle<T extends (...args: any) => any>(
62+
func: T,
63+
wait?: number,
64+
options?: ThrottleSettings,
65+
): DebouncedFunc<T> {
66+
return lodashThrottle(func, wait, options)
67+
}

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)