• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JedWatson / react-select / 14b9164f-7ebc-4af3-a108-73c5186d143a

pending completion
14b9164f-7ebc-4af3-a108-73c5186d143a

Pull #6045

circleci

jarodsim
feat: add changeset
Pull Request #6045: fix: allow loadOptions('') to be called when input is cleared

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.06
/packages/react-select/src/useAsync.ts
1
import { useCallback, useEffect, useRef, useState } from 'react';
2
import { handleInputChange } from './utils';
3
import { StateManagerProps } from './useStateManager';
4
import { GroupBase, InputActionMeta, OptionsOrGroups } from './types';
5

6
type AsyncManagedPropKeys =
7
  | 'options'
8
  | 'isLoading'
9
  | 'onInputChange'
10
  | 'filterOption';
11

12
export interface AsyncAdditionalProps<Option, Group extends GroupBase<Option>> {
13
  /**
14
   * The default set of options to show before the user starts searching. When
15
   * set to `true`, the results for loadOptions('') will be autoloaded.
16
   */
17
  defaultOptions?: OptionsOrGroups<Option, Group> | boolean;
18
  /**
19
   * If cacheOptions is truthy, then the loaded data will be cached. The cache
20
   * will remain until `cacheOptions` changes value.
21
   */
22
  cacheOptions?: any;
23
  /**
24
   * Function that returns a promise, which is the set of options to be used
25
   * once the promise resolves.
26
   */
27
  loadOptions?: (
28
    inputValue: string,
29
    callback: (options: OptionsOrGroups<Option, Group>) => void
30
  ) => Promise<OptionsOrGroups<Option, Group>> | void;
31
  /**
32
   * Will cause the select to be displayed in the loading state, even if the
33
   * Async select is not currently waiting for loadOptions to resolve
34
   */
35
  isLoading?: boolean;
36
  /**
37
   * When set to `true`, the `loadOptions` function will be called even when the input is empty (`''`).
38
   * This allows consumers to handle empty input searches (e.g., return a default set of results).
39
   * If `false` (default), the input will reset without triggering `loadOptions('')`.
40
   */
41
  allowEmptySearch?: boolean;
42
}
43

44
export type AsyncProps<
45
  Option,
46
  IsMulti extends boolean,
47
  Group extends GroupBase<Option>
48
> = StateManagerProps<Option, IsMulti, Group> &
49
  AsyncAdditionalProps<Option, Group>;
50

51
export default function useAsync<
52
  Option,
53
  IsMulti extends boolean,
54
  Group extends GroupBase<Option>,
55
  AdditionalProps
56
>({
57
  defaultOptions: propsDefaultOptions = false,
47✔
58
  cacheOptions = false,
45✔
59
  loadOptions: propsLoadOptions,
60
  options: propsOptions,
61
  isLoading: propsIsLoading = false,
56✔
62
  onInputChange: propsOnInputChange,
63
  filterOption = null,
56✔
64
  allowEmptySearch,
65
  ...restSelectProps
66
}: AsyncProps<Option, IsMulti, Group> & AdditionalProps): StateManagerProps<
67
  Option,
68
  IsMulti,
69
  Group
70
> &
71
  Omit<
72
    AdditionalProps,
73
    keyof AsyncAdditionalProps<Option, Group> | AsyncManagedPropKeys
74
  > {
75
  const { inputValue: propsInputValue } = restSelectProps;
56✔
76

77
  const lastRequest = useRef<unknown>(undefined);
56✔
78
  const mounted = useRef(false);
56✔
79

80
  const [defaultOptions, setDefaultOptions] = useState<
56✔
81
    OptionsOrGroups<Option, Group> | boolean | undefined
82
  >(Array.isArray(propsDefaultOptions) ? propsDefaultOptions : undefined);
56!
83
  const [stateInputValue, setStateInputValue] = useState<string>(
56✔
84
    typeof propsInputValue !== 'undefined' ? (propsInputValue as string) : ''
56✔
85
  );
86
  const [isLoading, setIsLoading] = useState(propsDefaultOptions === true);
56✔
87
  const [loadedInputValue, setLoadedInputValue] =
88
    useState<string | undefined>(undefined);
56✔
89
  const [loadedOptions, setLoadedOptions] = useState<
56✔
90
    OptionsOrGroups<Option, Group>
91
  >([]);
92
  const [passEmptyOptions, setPassEmptyOptions] = useState(false);
56✔
93
  const [optionsCache, setOptionsCache] = useState<
56✔
94
    Record<string, OptionsOrGroups<Option, Group>>
95
  >({});
96
  const [prevDefaultOptions, setPrevDefaultOptions] =
97
    useState<OptionsOrGroups<Option, Group> | boolean | undefined>(undefined);
56✔
98
  const [prevCacheOptions, setPrevCacheOptions] = useState(undefined);
56✔
99

100
  if (cacheOptions !== prevCacheOptions) {
56✔
101
    setOptionsCache({});
15✔
102
    setPrevCacheOptions(cacheOptions);
15✔
103
  }
104

105
  if (propsDefaultOptions !== prevDefaultOptions) {
56✔
106
    setDefaultOptions(
15✔
107
      Array.isArray(propsDefaultOptions) ? propsDefaultOptions : undefined
15!
108
    );
109
    setPrevDefaultOptions(propsDefaultOptions);
15✔
110
  }
111

112
  useEffect(() => {
56✔
113
    mounted.current = true;
15✔
114
    return () => {
15✔
115
      mounted.current = false;
15✔
116
    };
117
  }, []);
118

119
  const loadOptions = useCallback(
56✔
120
    (
121
      inputValue: string,
122
      callback: (options?: OptionsOrGroups<Option, Group>) => void
123
    ) => {
124
      if (!propsLoadOptions) return callback();
13✔
125
      const loader = propsLoadOptions(inputValue, callback);
12✔
126
      if (loader && typeof loader.then === 'function') {
12✔
127
        loader.then(callback, () => callback());
2✔
128
      }
129
    },
130
    [propsLoadOptions]
131
  );
132

133
  useEffect(() => {
56✔
134
    if (propsDefaultOptions === true) {
15✔
135
      loadOptions(stateInputValue, (options) => {
3✔
136
        if (!mounted.current) return;
2!
137
        setDefaultOptions(options || []);
2!
138
        setIsLoading(!!lastRequest.current);
2✔
139
      });
140
    }
141
    // NOTE: this effect is designed to only run when the component mounts,
142
    // so we don't want to include any hook dependencies
143
    // eslint-disable-next-line react-hooks/exhaustive-deps
144
  }, []);
145

146
  const onInputChange = useCallback(
56✔
147
    (newValue: string, actionMeta: InputActionMeta) => {
148
      const inputValue = handleInputChange(
11✔
149
        newValue,
150
        actionMeta,
151
        propsOnInputChange
152
      );
153
      if (!inputValue && !allowEmptySearch) {
11!
154
        lastRequest.current = undefined;
×
155
        setStateInputValue('');
×
156
        setLoadedInputValue('');
×
157
        setLoadedOptions([]);
×
158
        setIsLoading(false);
×
159
        setPassEmptyOptions(false);
×
160
        return;
×
161
      }
162
      if (cacheOptions && optionsCache[inputValue]) {
11✔
163
        setStateInputValue(inputValue);
1✔
164
        setLoadedInputValue(inputValue);
1✔
165
        setLoadedOptions(optionsCache[inputValue]);
1✔
166
        setIsLoading(false);
1✔
167
        setPassEmptyOptions(false);
1✔
168
      } else {
169
        const request = (lastRequest.current = {});
10✔
170
        setStateInputValue(inputValue);
10✔
171
        setIsLoading(true);
10✔
172
        setPassEmptyOptions(!loadedInputValue);
10✔
173
        loadOptions(inputValue, (options) => {
10✔
174
          if (!mounted) return;
8!
175
          if (request !== lastRequest.current) return;
8✔
176
          lastRequest.current = undefined;
7✔
177
          setIsLoading(false);
7✔
178
          setLoadedInputValue(inputValue);
7✔
179
          setLoadedOptions(options || []);
7✔
180
          setPassEmptyOptions(false);
7✔
181
          setOptionsCache(
7✔
182
            options ? { ...optionsCache, [inputValue]: options } : optionsCache
7✔
183
          );
184
        });
185
      }
186
    },
187
    [
188
      cacheOptions,
189
      loadOptions,
190
      loadedInputValue,
191
      optionsCache,
192
      propsOnInputChange,
193
      allowEmptySearch,
194
    ]
195
  );
196

197
  const options = passEmptyOptions
56✔
198
    ? []
199
    : stateInputValue && loadedInputValue
100✔
200
    ? loadedOptions
201
    : ((defaultOptions || []) as OptionsOrGroups<Option, Group>);
65✔
202

203
  return {
56✔
204
    ...restSelectProps,
205
    options,
206
    isLoading: isLoading || propsIsLoading,
99✔
207
    onInputChange,
208
    filterOption,
209
  };
210
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc