UNPKG

3.54 kBPlain TextView Raw
1/*
2 * Copyright 2024 Adobe. All rights reserved.
3 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may obtain a copy
5 * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software distributed under
8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 * OF ANY KIND, either express or implied. See the License for the specific language
10 * governing permissions and limitations under the License.
11 */
12
13import {RefObject, useCallback, useRef} from 'react';
14import {useEvent} from './useEvent';
15// eslint-disable-next-line rulesdir/useLayoutEffectRule
16import {useLayoutEffect} from './useLayoutEffect';
17
18export interface LoadMoreProps {
19 /** Whether data is currently being loaded. */
20 isLoading?: boolean,
21 /** Handler that is called when more items should be loaded, e.g. while scrolling near the bottom. */
22 onLoadMore?: () => void,
23 /**
24 * The amount of offset from the bottom of your scrollable region that should trigger load more.
25 * Uses a percentage value relative to the scroll body's client height. Load more is then triggered
26 * when your current scroll position's distance from the bottom of the currently loaded list of items is less than
27 * or equal to the provided value. (e.g. 1 = 100% of the scroll region's height).
28 * @default 1
29 */
30 scrollOffset?: number,
31 /** The data currently loaded. */
32 items?: any[]
33}
34
35export function useLoadMore(props: LoadMoreProps, ref: RefObject<HTMLElement | null>) {
36 let {isLoading, onLoadMore, scrollOffset = 1, items} = props;
37
38 // Handle scrolling, and call onLoadMore when nearing the bottom.
39 let isLoadingRef = useRef(isLoading);
40 let prevProps = useRef(props);
41 let onScroll = useCallback(() => {
42 if (ref.current && !isLoadingRef.current && onLoadMore) {
43 let shouldLoadMore = ref.current.scrollHeight - ref.current.scrollTop - ref.current.clientHeight < ref.current.clientHeight * scrollOffset;
44
45 if (shouldLoadMore) {
46 isLoadingRef.current = true;
47 onLoadMore();
48 }
49 }
50 }, [onLoadMore, ref, scrollOffset]);
51
52 let lastItems = useRef(items);
53 useLayoutEffect(() => {
54 // Only update isLoadingRef if props object actually changed,
55 // not if a local state change occurred.
56 if (props !== prevProps.current) {
57 isLoadingRef.current = isLoading;
58 prevProps.current = props;
59 }
60
61 // TODO: Eventually this hook will move back into RAC during which we will accept the collection as a option to this hook.
62 // We will only load more if the collection has changed after the last load to prevent multiple onLoadMore from being called
63 // while the data from the last onLoadMore is being processed by RAC collection.
64 let shouldLoadMore = ref?.current
65 && !isLoadingRef.current
66 && onLoadMore
67 && (!items || items !== lastItems.current)
68 && ref.current.clientHeight === ref.current.scrollHeight;
69
70 if (shouldLoadMore) {
71 isLoadingRef.current = true;
72 onLoadMore?.();
73 }
74
75 lastItems.current = items;
76 }, [isLoading, onLoadMore, props, ref]);
77
78 // TODO: maybe this should still just return scroll props?
79 // Test against case where the ref isn't defined when this is called
80 // Think this was a problem when trying to attach to the scrollable body of the table in OnLoadMoreTableBodyScroll
81 useEvent(ref, 'scroll', onScroll);
82}