| 279 | * to update the data over time. Manages loading and error states, pagination, and sorting. |
| 280 | */ |
| 281 | export function useAsyncList<T, C = string>(options: AsyncListOptions<T, C>): AsyncListData<T> { |
| 282 | const { |
| 283 | load, |
| 284 | sort, |
| 285 | initialSelectedKeys, |
| 286 | initialSortDescriptor, |
| 287 | getKey = (item: any) => item.id || item.key, |
| 288 | initialFilterText = '' |
| 289 | } = options; |
| 290 | |
| 291 | let [data, dispatch] = useReducer<AsyncListState<T, C>, [Action<T, C>]>(reducer, { |
| 292 | state: 'idle', |
| 293 | error: undefined, |
| 294 | items: [], |
| 295 | selectedKeys: initialSelectedKeys === 'all' ? 'all' : new Set(initialSelectedKeys), |
| 296 | sortDescriptor: initialSortDescriptor, |
| 297 | filterText: initialFilterText |
| 298 | }); |
| 299 | |
| 300 | const dispatchFetch = async (action: Action<T, C>, fn: AsyncListLoadFunction<T, C>) => { |
| 301 | let abortController = new AbortController(); |
| 302 | try { |
| 303 | dispatch({...action, abortController}); |
| 304 | let previousFilterText = action.filterText ?? data.filterText; |
| 305 | |
| 306 | let response = await fn({ |
| 307 | items: data.items.slice(), |
| 308 | selectedKeys: data.selectedKeys, |
| 309 | sortDescriptor: action.sortDescriptor ?? data.sortDescriptor, |
| 310 | signal: abortController.signal, |
| 311 | cursor: action.type === 'loadingMore' ? data.cursor : undefined, |
| 312 | filterText: previousFilterText, |
| 313 | loadingState: data.state |
| 314 | }); |
| 315 | |
| 316 | let filterText = response.filterText ?? previousFilterText; |
| 317 | dispatch({type: 'success', ...response, abortController}); |
| 318 | |
| 319 | // Fetch a new filtered list if filterText is updated via `load` response func rather than list.setFilterText |
| 320 | // Only do this if not aborted (e.g. user triggers another filter action before load completes) |
| 321 | if (filterText && filterText !== previousFilterText && !abortController.signal.aborted) { |
| 322 | dispatchFetch({type: 'filtering', filterText}, load); |
| 323 | } |
| 324 | } catch (e) { |
| 325 | dispatch({type: 'error', error: e as Error, abortController}); |
| 326 | } |
| 327 | }; |
| 328 | |
| 329 | let didDispatchInitialFetch = useRef(false); |
| 330 | useEffect(() => { |
| 331 | if (!didDispatchInitialFetch.current) { |
| 332 | dispatchFetch({type: 'loading'}, load); |
| 333 | didDispatchInitialFetch.current = true; |
| 334 | } |
| 335 | // eslint-disable-next-line react-hooks/exhaustive-deps |
| 336 | }, []); |
| 337 | |
| 338 | return { |