Skip to content

Commit d6cd19b

Browse files
authored
Merge pull request #364 from 10up/feature/content-search-debounce
Convert ContentSearch input field from useState to useDebouncedInput to minimize consequtive search requests
2 parents 4ba3e66 + f2231a0 commit d6cd19b

File tree

8 files changed

+100
-15
lines changed

8 files changed

+100
-15
lines changed

components/content-picker/index.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ const ContentPickerWrapper = styled.div`
3535
width: 100%;
3636
`;
3737

38-
interface ContentPickerProps {
38+
export type ContentPickerOptions = {
39+
inputDelay: number;
40+
};
41+
42+
export interface ContentPickerProps {
3943
label?: string;
4044
hideLabelFromVision?: boolean;
4145
mode?: ContentSearchMode;
@@ -55,6 +59,7 @@ interface ContentPickerProps {
5559
renderItemType?: (props: NormalizedSuggestion) => string;
5660
renderItem?: (props: RenderItemComponentProps) => JSX.Element;
5761
PickedItemPreviewComponent?: React.ComponentType<{ item: PickedItemType }>;
62+
options?: ContentPickerOptions;
5863
}
5964

6065
export const ContentPicker: React.FC<ContentPickerProps> = ({
@@ -79,7 +84,10 @@ export const ContentPicker: React.FC<ContentPickerProps> = ({
7984
renderItemType = defaultRenderItemType,
8085
renderItem = undefined,
8186
PickedItemPreviewComponent = undefined,
87+
options,
8288
}) => {
89+
const searchOptions =
90+
options && options.inputDelay ? { inputDelay: options.inputDelay } : undefined;
8391
const currentPostId = select('core/editor')?.getCurrentPostId();
8492

8593
/**
@@ -147,6 +155,7 @@ export const ContentPicker: React.FC<ContentPickerProps> = ({
147155
fetchInitialResults={fetchInitialResults}
148156
renderItemType={renderItemType}
149157
renderItem={renderItem}
158+
options={searchOptions}
150159
/>
151160
) : (
152161
label && (

components/content-picker/readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ function MyComponent( props ) {
4040
| `content` | `array` | `[]` | Array of items to pre-populate picker with. Must be in the format of: `[{id: 1, type: 'post', uuid: '...',}, {id: 1, uuid: '...', type: 'page'},... ]`. You cannot provide terms and posts to the same picker. `uuid` was added as of version 1.5.0. It is only used as the React component list key in the admin. If it is not included, `id` will be used which will cause errors if you select the same post twice. |
4141
| `perPage` | `number` | `50` | Number of items to show during search
4242
| `fetchInitialResults` | `bool` | `false` | Fetch initial results to present when focusing the search input |
43+
| `options.inputDelay` | `number` | `undefined` | Debounce delay passed to the internal search input, defaults to 350ms |
4344
| `PickedItemPreviewComponent` | `React.ComponentType<item>` | `undefined` | Allow replacing the default picked item preview. The `item` prop includes information about the selected entry (please check the `PickedItemType` interface in `./PickedItem.tsx`). | |
4445
__NOTE:__ Content picker cannot validate that posts you pass it via `content` prop actually exist. If a post does not exist, it will not render as one of the picked items but will still be passed back as picked items if new items are picked/sorted. Therefore, on save you need to validate that all the picked posts/terms actually exist.
4546

components/content-search/index.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
QueryFilter,
1313
RenderItemComponentProps,
1414
} from './types';
15+
import { useDebouncedInput } from '../../hooks/use-debounced-input';
1516
import { useOnClickOutside } from '../../hooks/use-on-click-outside';
1617
import { NormalizedSuggestion, fetchSearchResults } from './utils';
1718

@@ -65,11 +66,9 @@ const StyledNoResults = styled.li`
6566
padding-left: 3px;
6667
`;
6768

68-
const ContentSearchNoResults: React.FC = () => (
69-
<StyledNoResults className="tenup-content-search-list-item components-button">
70-
{__('Nothing found.', '10up-block-components')}
71-
</StyledNoResults>
72-
);
69+
export type ContentSearchOptions = {
70+
inputDelay: number;
71+
};
7372

7473
export interface ContentSearchProps {
7574
onSelectItem: (item: NormalizedSuggestion) => void;
@@ -84,8 +83,15 @@ export interface ContentSearchProps {
8483
renderItemType?: (props: NormalizedSuggestion) => string;
8584
renderItem?: (props: RenderItemComponentProps) => JSX.Element;
8685
fetchInitialResults?: boolean;
86+
options?: ContentSearchOptions;
8787
}
8888

89+
const ContentSearchNoResults: React.FC = () => (
90+
<StyledNoResults className="tenup-content-search-list-item components-button">
91+
{__('Nothing found.', '10up-block-components')}
92+
</StyledNoResults>
93+
);
94+
8995
const ContentSearch: React.FC<ContentSearchProps> = ({
9096
onSelectItem = () => {
9197
console.log('Select!'); // eslint-disable-line no-console
@@ -101,8 +107,11 @@ const ContentSearch: React.FC<ContentSearchProps> = ({
101107
renderItemType = undefined,
102108
renderItem: SearchResultItem = SearchItem,
103109
fetchInitialResults,
110+
options,
104111
}) => {
105-
const [searchString, setSearchString] = useState('');
112+
const debounceOptions =
113+
options && options.inputDelay ? { delay: options.inputDelay } : undefined;
114+
const [searchInput, setSearchString, searchString] = useDebouncedInput('', debounceOptions);
106115
const [isFocused, setIsFocused] = useState(false);
107116
const searchContainer = useRef<HTMLDivElement>(null);
108117

@@ -148,7 +157,7 @@ const ContentSearch: React.FC<ContentSearchProps> = ({
148157
return (
149158
<StyledNavigableMenu ref={mergedRef} orientation="vertical">
150159
<StyledSearchControl
151-
value={searchString}
160+
value={searchInput}
152161
onChange={(newSearchString: string) => {
153162
setSearchString(newSearchString);
154163
}}

components/content-search/readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@ function MyComponent( props ) {
3535
| `perPage` | `number` | `50` | Number of items to show during search |
3636
| `renderItemType` | `function` | `undefined` | Function called to override the item type label in `SearchItem`. Must return the new label. |
3737
| `fetchInitialResults` | `bool` | `false` | Fetch initial results to present when focusing the search input |
38+
| `options.inputDelay` | `number` | `undefined` | Debounce delay passed to the internal search input, defaults to 350ms |

components/media-toolbar/index.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,19 +72,15 @@ export const MediaToolbar: React.FC<MediaToolbarProps> = ({
7272
name={mergedLabels.replace}
7373
/>
7474
{!!isOptional && (
75-
<ToolbarButton onClick={onRemove}>
76-
{mergedLabels.remove}
77-
</ToolbarButton>
75+
<ToolbarButton onClick={onRemove}>{mergedLabels.remove}</ToolbarButton>
7876
)}
7977
</>
8078
) : (
8179
<MediaUploadCheck>
8280
<MediaUpload
8381
onSelect={onSelect}
8482
render={({ open }) => (
85-
<ToolbarButton onClick={open}>
86-
{mergedLabels.add}
87-
</ToolbarButton>
83+
<ToolbarButton onClick={open}>{mergedLabels.add}</ToolbarButton>
8884
)}
8985
/>
9086
</MediaUploadCheck>

components/post-featured-image/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { useEntityProp } from '@wordpress/core-data';
22
import { usePost } from '../../hooks';
33
import { Image } from '../image';
44

5-
interface PostFeaturedImageProps extends Omit<React.ComponentProps<typeof Image>, 'id' | 'onSelect' | 'canEditImage'> {}
5+
interface PostFeaturedImageProps
6+
extends Omit<React.ComponentProps<typeof Image>, 'id' | 'onSelect' | 'canEditImage'> {}
67

78
export const PostFeaturedImage = (props: PostFeaturedImageProps) => {
89
const { postId, postType, isEditable } = usePost();

hooks/use-debounced-input/index.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useEffect, useState } from '@wordpress/element';
2+
import { useDebounce } from '@wordpress/compose';
3+
4+
type DebouncedInputOptions = {
5+
delay: number;
6+
};
7+
8+
/**
9+
* Helper hook for input fields that need to debounce the value before using it.
10+
*
11+
* @param {string} defaultValue The default value to use.
12+
* @param {DebouncedInputOptions} options Set of options for useDebounce, 350ms is the default
13+
*
14+
* @returns The input value, the setter and the debounced input value.
15+
*/
16+
export function useDebouncedInput(
17+
defaultValue: string = '',
18+
options: DebouncedInputOptions = { delay: 350 },
19+
): [string, (value: string) => void, string] {
20+
const [input, setInput] = useState<string>(defaultValue);
21+
const [debouncedInput, setDebouncedState] = useState(defaultValue);
22+
const { delay } = options;
23+
const setDebouncedInput = useDebounce(setDebouncedState, delay);
24+
25+
useEffect(() => {
26+
setDebouncedInput(input);
27+
}, [input, setDebouncedInput]);
28+
29+
return [input, setInput, debouncedInput];
30+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# `useDebouncedInput`
2+
3+
The `useDebouncedInput` hook is a revision of the `@wordpress/components` version of the hook which exposes options, specifically to configure the debounce delay.
4+
5+
## Usage
6+
7+
```js
8+
import { SearchControl } from '@wordpress/components';
9+
import { useDebouncedInput } from '@10up/block-components';
10+
11+
function BlockEdit(props) {
12+
const [searchInput, setSearchString, searchString] = useDebouncedInput('');
13+
...
14+
async fetchTitles => {
15+
let options = await apiFetch({
16+
path: addQueryArgs('/wp/v2/search', {
17+
search: searchString,
18+
...
19+
})
20+
});
21+
options = options.filter(option => option.title !== '');
22+
23+
return options;
24+
};
25+
...
26+
return (
27+
<>
28+
<SearchControl
29+
value={searchInput}
30+
onChange={(newSearchString: string) => {
31+
setSearchString(newSearchString);
32+
}}
33+
/>
34+
...
35+
</>
36+
);
37+
}
38+
```

0 commit comments

Comments
 (0)