Skip to content

Commit

Permalink
Merge pull request #831 from thundersdata-frontend/hooks-issue
Browse files Browse the repository at this point in the history
feat: 新增useInfiniteScroll
  • Loading branch information
chj-damon authored Jan 31, 2024
2 parents 6f026f9 + 7c28a40 commit 299399a
Show file tree
Hide file tree
Showing 7 changed files with 2,000 additions and 79 deletions.
5 changes: 5 additions & 0 deletions .changeset/pretty-rings-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@td-design/rn-hooks': minor
---

feat: 新增useInfiniteScroll
3 changes: 3 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@ module.exports = {
moduleNameMapper: {
'^lodash-es$': 'lodash',
},
globals: {
'__DEV__': true,
}
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"prettier-plugin-packagejson": "^2.4.5",
"raf": "^3.4.1",
"react": "^17.0.0",
"react-native": "^0.72.9",
"react-test-renderer": "^17.0.0",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/hooks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { default as useDimensions } from './useDimensions';
import { default as useDynamicList } from './useDynamicList';
import { default as useEventEmitter } from './useEventEmitter';
import { default as useHistoryTravel } from './useHistoryTravel';
import { default as useInfiniteScroll } from './useInfiniteScroll';
import { default as useInterval } from './useInterval';
import { default as useKeyboard } from './useKeyboard';
import { default as useLatest } from './useLatest';
Expand Down Expand Up @@ -91,4 +92,5 @@ export {
useWhyDidYouUpdate,
useSms,
clearCache,
useInfiniteScroll,
};
150 changes: 150 additions & 0 deletions packages/hooks/src/useInfiniteScroll/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { useState } from 'react';

import { act, renderHook } from '@testing-library/react-hooks';

import { sleep } from '../../utils/testHelpers';
import useInfiniteScroll from '../index';

async function mockRequest({ page, pageSize }: { page: number; pageSize: number }) {
await sleep(1000);
return {
page,
pageSize,
total: 30,
list: Array(10)
.fill('')
.map((_, index) => ({ id: (page - 1) * pageSize + index, name: `Cell${(page - 1) * pageSize + index}` })),
};
}

const setup = (service, options?: any) => renderHook(() => useInfiniteScroll(service, options));

describe('useInfiniteScroll', () => {
beforeAll(() => {
jest.useFakeTimers();
});

afterAll(() => {
jest.useRealTimers();
});

it('should auto load', async () => {
const { result } = setup(mockRequest);
expect(result.current.loading).toBeTruthy();
await act(async () => {
jest.advanceTimersByTime(1000);
});
expect(result.current.loading).toBeFalsy();
});

it('loadMore should work', async () => {
const { result } = setup(mockRequest);
expect(result.current.loading).toBeTruthy();

act(() => {
result.current.loadMore();
});

expect(result.current.loadingMore).toBeTruthy();
await act(async () => {
jest.advanceTimersByTime(1000);
});
expect(result.current.loadingMore).toBeFalsy();
});

it('refresh should work', async () => {
const fn = jest.fn(() => Promise.resolve({ list: [] }));
const { result } = setup(fn);
const { refresh } = result.current;
expect(fn).toBeCalledTimes(1);
await act(async () => {
refresh();
});
expect(fn).toBeCalledTimes(2);
});

it('refresh should be triggered when refreshDeps change', async () => {
const fn = jest.fn(() => Promise.resolve({ list: [], page: 1, pageSize: 10, total: 30 }));
const { result } = renderHook(() => {
const [value, setValue] = useState('');
const res = useInfiniteScroll(fn, {
refreshDeps: [value],
});
return {
...res,
setValue,
};
});
expect(fn).toBeCalledTimes(1);
act(() => {
result.current.setValue('ahooks');
});
expect(fn).toBeCalledTimes(2);
});

it('cancel should be work', () => {
const onSuccess = jest.fn();
const { result } = setup(mockRequest, {
onSuccess,
});
const { cancel } = result.current;
expect(result.current.loading).toBe(true);
act(() => cancel());
expect(result.current.loading).toBe(false);
expect(onSuccess).not.toBeCalled();
});

it('onBefore/onSuccess/onFinally should be called', async () => {
const onBefore = jest.fn();
const onSuccess = jest.fn();
const onFinally = jest.fn();
const { result } = setup(mockRequest, {
onBefore,
onSuccess,
onFinally,
});
await act(async () => {
jest.advanceTimersByTime(1000);
});
expect(onBefore).toBeCalled();
expect(onSuccess).toBeCalled();
expect(onFinally).toBeCalled();
});

it('onError should be called when throw error', async () => {
const onError = jest.fn();
const mockRequestError = () => {
return Promise.reject('error');
};
setup(mockRequestError, {
onError,
});
await act(async () => {
Promise.resolve();
});
expect(onError).toBeCalled();
});

it('loading should be true when refresh after loadMore', async () => {
const { result } = setup(mockRequest);
expect(result.current.loading).toBeTruthy();
const { refresh, loadMore } = result.current;
await act(async () => {
jest.advanceTimersByTime(1000);
});

expect(result.current.loading).toBeFalsy();

act(() => {
loadMore();
refresh();
});
expect(result.current.loading).toBeTruthy();

await act(async () => {
jest.advanceTimersByTime(1000);
});

expect(result.current.loading).toBeFalsy();
});
});
119 changes: 119 additions & 0 deletions packages/hooks/src/useInfiniteScroll/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { DependencyList } from 'react';

import useMemoizedFn from '../useMemoizedFn';
import useRequest from '../useRequest';
import useSafeState from '../useSafeState';
import useUpdateEffect from '../useUpdateEffect';

interface PageParams {
page: number;
pageSize: number;
}

interface Page<T> extends PageParams {
list: T[];
total: number;
totalPage?: number;
}

interface InfiniteScrollOptions<TData> {
manual?: boolean;
refreshDeps?: DependencyList;

onBefore?: () => void;
onSuccess?: (data: TData) => void;
onError?: (error: Error) => void;
onFinally?: (data?: TData, error?: Error) => void;
}

const INITIAL_PAGE = 1;
const INITIAL_PAGE_SIZE = 10;

function useInfiniteScroll<T>(
service: (data: PageParams) => Promise<Page<T>>,
options?: InfiniteScrollOptions<Page<T>>
) {
const { manual = false, refreshDeps = [], onBefore, onSuccess, onError, onFinally } = options || {};

const [data, setData] = useSafeState<Page<T>>();
const [loadingMore, setLoadingMore] = useSafeState(false);
const [noMoreData, setNoMoreData] = useSafeState(false);

const { loading, error, run, runAsync, cancel } = useRequest(
async (lastData: Page<T>) => {
const currentData = await service(
lastData
? {
page: lastData.page + 1,
pageSize: lastData.pageSize,
}
: {
page: INITIAL_PAGE,
pageSize: INITIAL_PAGE_SIZE,
}
);

if (!currentData) {
setNoMoreData(true);
return currentData;
}

setNoMoreData(currentData.page * currentData.pageSize >= currentData.total);

if (!lastData) {
setData({
...currentData,
list: [...(currentData.list || [])],
});
} else {
setData({
...currentData,
list: [...(lastData.list || []), ...(currentData.list || [])],
});
}

return currentData;
},
{
manual,
onBefore,
onSuccess,
onError,
onFinally(_, d, e) {
setLoadingMore(false);
onFinally?.(d, e);
},
}
);

const loadMore = useMemoizedFn(() => {
if (noMoreData) return;

setLoadingMore(true);
return run(data);
});

const refresh = useMemoizedFn(() => {
setLoadingMore(false);
return runAsync();
});

useUpdateEffect(() => {
run();
}, [...refreshDeps]);

return {
data: data?.list || [],
loading,
loadingMore,
noMoreData,
error,

loadMore,
refresh,
cancel,
mutate: setData,
};
}

export default useInfiniteScroll;
Loading

0 comments on commit 299399a

Please sign in to comment.