From 76e0c097dc6adb112cc4fd0f7f3385e68cd911e0 Mon Sep 17 00:00:00 2001 From: John Date: Sat, 27 Feb 2021 16:47:35 +0800 Subject: [PATCH 1/3] feat: add useLoadMore --- example/App.tsx | 57 ++++++++++++++--- src/core/createQuery.ts | 8 ++- src/core/useAsyncQuery.ts | 2 + src/index.ts | 1 + src/useLoadMore.ts | 129 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 188 insertions(+), 9 deletions(-) create mode 100644 src/useLoadMore.ts diff --git a/example/App.tsx b/example/App.tsx index f406800..5efa93a 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -1,10 +1,31 @@ -import { defineComponent } from 'vue'; -import { useRequest } from 'vue-request'; +import { defineComponent, watchEffect } from 'vue'; +import { useLoadMore } from 'vue-request'; +import Mock from 'mockjs'; -function testService() { - return new Promise(resolve => { +function api(current: number, total = 10) { + let list: string[] = []; + if (current <= total) { + list = Mock.mock({ + 'list|5': ['@name'], + }).list; + } else { + list = []; + } + return { + current, + total, + list, + }; +} +type APIReturnType = ReturnType; +type rawDataType = { + data: APIReturnType; + dataList: APIReturnType['list']; +}; +function testService(rawData: rawDataType) { + return new Promise(resolve => { setTimeout(() => { - resolve('success'); + resolve(api((rawData?.data.current || 0) + 1)); }, 1000); }); } @@ -12,12 +33,32 @@ function testService() { export default defineComponent({ name: 'App', setup() { - const { run, data, loading } = useRequest(testService); + const { loadMore, loadingMore, dataList, noMore } = useLoadMore( + testService, + { + isNoMore: d => d?.current > d?.total, + formatResult: d => d.list, + }, + ); + return () => (
- +
- {loading.value ? 'loading...' : data.value} + {loadingMore.value + ? 'loading...' + : dataList.value?.map((i, idx) => ( +
+

{idx}

: {i} +
+ ))}
); }, diff --git a/src/core/createQuery.ts b/src/core/createQuery.ts index db7fcc2..b52f5cd 100644 --- a/src/core/createQuery.ts +++ b/src/core/createQuery.ts @@ -21,6 +21,7 @@ export interface Mutate extends MutateData, MutateFunction {} export type State = { loading: Ref; data: Ref; + _raw_data: Ref; error: Ref; params: Ref

; }; @@ -58,7 +59,7 @@ const setStateBind = >( const createQuery = ( query: Query, config: Config, - initialState?: UnWrapRefObject>, + initialState?: UnWrapRefObject, '_raw_data'>>, ): InnerQueryState => { const { initialAutoRunFlag, @@ -81,6 +82,7 @@ const createQuery = ( const retriedCount = ref(0); const loading = ref(initialState?.loading ?? false); const data = ref(initialState?.data ?? initialData) as Ref; + const _raw_data = ref(initialState?.data ?? initialData) as Ref; const error = ref(initialState?.error); const params = ref(initialState?.params ?? []) as Ref

; @@ -90,6 +92,7 @@ const createQuery = ( data, error, params, + _raw_data, }, [state => updateCache(state)], ); @@ -197,6 +200,7 @@ const createQuery = ( const formattedResult = formatResult ? formatResult(res) : res; setState({ + _raw_data: res, data: formattedResult, loading: false, error: undefined, @@ -214,6 +218,7 @@ const createQuery = ( .catch(error => { if (currentCount === count.value) { setState({ + _raw_data: undefined, data: undefined, loading: false, error: error, @@ -292,6 +297,7 @@ const createQuery = ( return { loading, data, + _raw_data, error, params, run, diff --git a/src/core/useAsyncQuery.ts b/src/core/useAsyncQuery.ts index a919e5e..7457719 100644 --- a/src/core/useAsyncQuery.ts +++ b/src/core/useAsyncQuery.ts @@ -149,6 +149,7 @@ function useAsyncQuery( const loading = ref(false); const data = ref(); + const _raw_data = ref(); const error = ref(); const params = ref

(); @@ -302,6 +303,7 @@ function useAsyncQuery( return { loading, data, + _raw_data, error, params, cancel: latestQuery.value.cancel, diff --git a/src/index.ts b/src/index.ts index eb8b2c9..426fdaf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,3 +2,4 @@ export { setGlobalOptions } from './core/config'; export { default as RequestConfig } from './RequestConfig'; export { default as useRequest } from './useRequest'; export { default as usePagination } from './usePagination'; +export { default as useLoadMore } from './useLoadMore'; diff --git a/src/useLoadMore.ts b/src/useLoadMore.ts new file mode 100644 index 0000000..778bca4 --- /dev/null +++ b/src/useLoadMore.ts @@ -0,0 +1,129 @@ +import { computed, ref, Ref, watchEffect } from 'vue'; +import { BaseOptions, FormatOptions } from './core/config'; +import useAsyncQuery, { BaseResult } from './core/useAsyncQuery'; +import get from 'lodash/get'; +import generateService from './core/utils/generateService'; +import { isFunction } from './core/utils'; +import { ServiceParams } from './core/utils/types'; +import { Query } from './core/createQuery'; + +export interface LoadMoreResult + extends Omit, 'queries' | '_raw_data'> { + dataList: Ref; + noMore: Ref; + loadingMore: Ref; + loadMore: () => void; + reload: () => void; +} + +export interface LoadMoreExtendsOption { + isNoMore: (data: R) => boolean; +} + +export type LoadMoreService = + | ((r: { data: R; dataList: FR }, ...args: P) => Promise) + | ((r: { data: R; dataList: FR }, ...args: P) => ServiceParams); + +export type LoadMoreFormatOptions = Omit< + FormatOptions, + 'queryKey' +> & + LoadMoreExtendsOption; + +export type LoadMoreBaseOptions = Omit< + BaseOptions, + 'queryKey' +> & + LoadMoreExtendsOption; + +export type LoadMoreMixinOptions = + | LoadMoreBaseOptions + | LoadMoreFormatOptions; + +function useLoadMore( + service: LoadMoreService, +): LoadMoreResult; +function useLoadMore( + service: LoadMoreService, + options: LoadMoreFormatOptions, +): LoadMoreResult; +function useLoadMore( + service: LoadMoreService, + options: LoadMoreBaseOptions, +): LoadMoreResult; +function useLoadMore( + service: LoadMoreService, + options?: LoadMoreMixinOptions, +) { + if (!isFunction(service)) { + throw new Error('useLoadMore only support function service'); + } + const promiseQuery = generateService(service as any); + + const { queryKey, isNoMore, ...restOptions } = options ?? ({} as any); + + if (queryKey) { + throw new Error('useLoadMore does not support concurrent request'); + } + + const loadingMore = ref(false); + const increaseQueryKey = ref(0); + const { data, params, run, queries, _raw_data, ...rest } = useAsyncQuery< + R, + P, + FR + >(promiseQuery, { + ...restOptions, + onSuccess: (...p) => { + loadingMore.value = false; + increaseQueryKey.value++; + restOptions?.onSuccess?.(...p); + }, + queryKey: () => String(increaseQueryKey.value), + }); + + const noMore = computed(() => { + return isNoMore ? isNoMore(_raw_data.value) : false; + }); + + const dataList = computed(() => { + let list: any[] = []; + Object.values(queries).forEach(h => { + const queriesData = h.data as any; + if (queriesData.value && Array.isArray(queriesData.value)) { + list = list.concat(queriesData.value); + } + }); + return (list as unknown) as R; + }); + + const loadMore = () => { + if (noMore.value) { + return; + } + loadingMore.value = true; + const [, ...restParams] = params.value; + const mergerParams = [ + { dataList: dataList.value, data: _raw_data.value }, + ...restParams, + ] as any; + run(...mergerParams); + }; + + const reload = () => {}; + + return { + // 每次 LoadMore 触发时,data 都会变成undefined,原因是 queries + data: _raw_data, + dataList, + params, + noMore, + loadingMore, + run, + reload, + loadMore, + ...rest, + }; +} + +export default useLoadMore; From fcad5c7276dd24892a6702abd4376a37f42cd858 Mon Sep 17 00:00:00 2001 From: John Date: Mon, 1 Mar 2021 18:57:38 +0800 Subject: [PATCH 2/3] refactor: update --- example/App.tsx | 39 +++++++++++++++----- src/core/createQuery.ts | 8 +---- src/core/useAsyncQuery.ts | 21 +++++++++-- src/core/utils/cache.ts | 13 ++++--- src/useLoadMore.ts | 76 ++++++++++++++++++++++++++------------- 5 files changed, 111 insertions(+), 46 deletions(-) diff --git a/example/App.tsx b/example/App.tsx index 5efa93a..ca82e09 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -1,4 +1,4 @@ -import { defineComponent, watchEffect } from 'vue'; +import { defineComponent, reactive, watchEffect } from 'vue'; import { useLoadMore } from 'vue-request'; import Mock from 'mockjs'; @@ -22,10 +22,14 @@ type rawDataType = { data: APIReturnType; dataList: APIReturnType['list']; }; + function testService(rawData: rawDataType) { + const current = (rawData?.data?.current || 0) + 1; + console.log(`${current}`, rawData); + return new Promise(resolve => { setTimeout(() => { - resolve(api((rawData?.data.current || 0) + 1)); + resolve(api(current)); }, 1000); }); } @@ -33,13 +37,21 @@ function testService(rawData: rawDataType) { export default defineComponent({ name: 'App', setup() { - const { loadMore, loadingMore, dataList, noMore } = useLoadMore( - testService, - { - isNoMore: d => d?.current > d?.total, - formatResult: d => d.list, + const { loadMore, loadingMore, dataList, noMore, reload } = useLoadMore< + APIReturnType, + any, + APIReturnType['list'] + >(testService, { + isNoMore: d => { + return d?.current >= d?.total; }, - ); + }); + + const testReactive = reactive({ first: 'init' }); + + watchEffect(() => { + console.log(testReactive); + }); return () => (

@@ -47,10 +59,21 @@ export default defineComponent({ disabled={noMore.value} onClick={() => { loadMore(); + Object.keys(testReactive).forEach(key => { + testReactive[key] = new Date(); + }); }} > run + +
{loadingMore.value ? 'loading...' diff --git a/src/core/createQuery.ts b/src/core/createQuery.ts index b52f5cd..db7fcc2 100644 --- a/src/core/createQuery.ts +++ b/src/core/createQuery.ts @@ -21,7 +21,6 @@ export interface Mutate extends MutateData, MutateFunction {} export type State = { loading: Ref; data: Ref; - _raw_data: Ref; error: Ref; params: Ref

; }; @@ -59,7 +58,7 @@ const setStateBind = >( const createQuery = ( query: Query, config: Config, - initialState?: UnWrapRefObject, '_raw_data'>>, + initialState?: UnWrapRefObject>, ): InnerQueryState => { const { initialAutoRunFlag, @@ -82,7 +81,6 @@ const createQuery = ( const retriedCount = ref(0); const loading = ref(initialState?.loading ?? false); const data = ref(initialState?.data ?? initialData) as Ref; - const _raw_data = ref(initialState?.data ?? initialData) as Ref; const error = ref(initialState?.error); const params = ref(initialState?.params ?? []) as Ref

; @@ -92,7 +90,6 @@ const createQuery = ( data, error, params, - _raw_data, }, [state => updateCache(state)], ); @@ -200,7 +197,6 @@ const createQuery = ( const formattedResult = formatResult ? formatResult(res) : res; setState({ - _raw_data: res, data: formattedResult, loading: false, error: undefined, @@ -218,7 +214,6 @@ const createQuery = ( .catch(error => { if (currentCount === count.value) { setState({ - _raw_data: undefined, data: undefined, loading: false, error: error, @@ -297,7 +292,6 @@ const createQuery = ( return { loading, data, - _raw_data, error, params, run, diff --git a/src/core/useAsyncQuery.ts b/src/core/useAsyncQuery.ts index 7457719..2698b91 100644 --- a/src/core/useAsyncQuery.ts +++ b/src/core/useAsyncQuery.ts @@ -29,7 +29,7 @@ import { resolvedPromise, unRefObject, } from './utils'; -import { getCache, setCache } from './utils/cache'; +import { getCache, setCache, clearCache } from './utils/cache'; import limitTrigger from './utils/limitTrigger'; import subscriber from './utils/listener'; import { UnWrapRefObject } from './utils/types'; @@ -39,6 +39,7 @@ export type BaseResult = Omit< 'run' > & { run: (...arg: P) => InnerRunReturn; + reset: () => void; }; export type UnWrapState = UnWrapRefObject< @@ -149,7 +150,6 @@ function useAsyncQuery( const loading = ref(false); const data = ref(); - const _raw_data = ref(); const error = ref(); const params = ref

(); @@ -300,16 +300,31 @@ function useAsyncQuery( unsubscribeList.forEach(unsubscribe => unsubscribe()); }); + const reset = () => { + latestQueriesKey.value = QUERY_DEFAULT_KEY; + Object.keys(queries).forEach(key => { + queries[key].cancel(); + if (key !== QUERY_DEFAULT_KEY) { + delete queries[key]; + } + }); + queries[QUERY_DEFAULT_KEY] = >( + reactive(createQuery(query, config)) + ); + cacheKey && clearCache(cacheKey); + unsubscribeList.forEach(unsubscribe => unsubscribe()); + }; + return { loading, data, - _raw_data, error, params, cancel: latestQuery.value.cancel, refresh: latestQuery.value.refresh, mutate: latestQuery.value.mutate, run, + reset, queries, }; } diff --git a/src/core/utils/cache.ts b/src/core/utils/cache.ts index b31a5db..fccda8b 100644 --- a/src/core/utils/cache.ts +++ b/src/core/utils/cache.ts @@ -48,8 +48,13 @@ export const setCache = ( }); }; -export const clearCache = () => { - // clear timer - CACHE_MAP.forEach(i => clearTimeout(i.timer)); - CACHE_MAP.clear(); +export const clearCache = (cacheKey?: CacheKey) => { + if (cacheKey) { + clearTimeout(CACHE_MAP.get(cacheKey)?.timer); + CACHE_MAP.delete(cacheKey); + } else { + // clear timer + CACHE_MAP.forEach(i => clearTimeout(i.timer)); + CACHE_MAP.clear(); + } }; diff --git a/src/useLoadMore.ts b/src/useLoadMore.ts index 778bca4..616ab45 100644 --- a/src/useLoadMore.ts +++ b/src/useLoadMore.ts @@ -7,9 +7,9 @@ import { isFunction } from './core/utils'; import { ServiceParams } from './core/utils/types'; import { Query } from './core/createQuery'; -export interface LoadMoreResult - extends Omit, 'queries' | '_raw_data'> { - dataList: Ref; +export interface LoadMoreResult + extends Omit, 'queries'> { + dataList: Ref; noMore: Ref; loadingMore: Ref; loadMore: () => void; @@ -18,6 +18,7 @@ export interface LoadMoreResult export interface LoadMoreExtendsOption { isNoMore: (data: R) => boolean; + listKey?: string; } export type LoadMoreService = @@ -40,19 +41,30 @@ export type LoadMoreMixinOptions = | LoadMoreBaseOptions | LoadMoreFormatOptions; -function useLoadMore( - service: LoadMoreService, -): LoadMoreResult; -function useLoadMore( - service: LoadMoreService, +function useLoadMore< + R, + P extends unknown[] = any, + LR extends unknown[] = any[] +>(service: LoadMoreService): LoadMoreResult; +function useLoadMore< + R, + P extends unknown[] = any, + FR = any, + LR extends unknown[] = any[] +>( + service: LoadMoreService, options: LoadMoreFormatOptions, -): LoadMoreResult; -function useLoadMore( - service: LoadMoreService, +): LoadMoreResult; +function useLoadMore< + R, + P extends unknown[] = any, + LR extends unknown[] = any[] +>( + service: LoadMoreService, options: LoadMoreBaseOptions, -): LoadMoreResult; -function useLoadMore( - service: LoadMoreService, +): LoadMoreResult; +function useLoadMore( + service: LoadMoreService, options?: LoadMoreMixinOptions, ) { if (!isFunction(service)) { @@ -60,7 +72,8 @@ function useLoadMore( } const promiseQuery = generateService(service as any); - const { queryKey, isNoMore, ...restOptions } = options ?? ({} as any); + const { queryKey, isNoMore, listKey = 'list', ...restOptions } = + options ?? ({} as any); if (queryKey) { throw new Error('useLoadMore does not support concurrent request'); @@ -68,7 +81,7 @@ function useLoadMore( const loadingMore = ref(false); const increaseQueryKey = ref(0); - const { data, params, run, queries, _raw_data, ...rest } = useAsyncQuery< + const { data, params, queries, run, reset, ...rest } = useAsyncQuery< R, P, FR @@ -82,19 +95,27 @@ function useLoadMore( queryKey: () => String(increaseQueryKey.value), }); + const latestData = ref(data.value) as Ref; + watchEffect(() => { + if (data.value !== undefined) { + latestData.value = data.value; + } + }); + const noMore = computed(() => { - return isNoMore ? isNoMore(_raw_data.value) : false; + return isNoMore ? isNoMore(latestData.value) : false; }); const dataList = computed(() => { let list: any[] = []; Object.values(queries).forEach(h => { const queriesData = h.data as any; - if (queriesData.value && Array.isArray(queriesData.value)) { - list = list.concat(queriesData.value); + const dataList = get(queriesData.value, listKey); + if (dataList && Array.isArray(dataList)) { + list = list.concat(dataList); } }); - return (list as unknown) as R; + return (list as unknown) as LR; }); const loadMore = () => { @@ -104,24 +125,31 @@ function useLoadMore( loadingMore.value = true; const [, ...restParams] = params.value; const mergerParams = [ - { dataList: dataList.value, data: _raw_data.value }, + { dataList: dataList.value, data: latestData.value }, ...restParams, ] as any; run(...mergerParams); }; - const reload = () => {}; + const reload = () => { + reset(); + increaseQueryKey.value = 0; + const [, ...restParams] = params.value; + const mergerParams = [undefined, ...restParams] as any; + run(...mergerParams); + }; return { // 每次 LoadMore 触发时,data 都会变成undefined,原因是 queries - data: _raw_data, - dataList, + data: latestData, + dataList: dataList, params, noMore, loadingMore, run, reload, loadMore, + reset, ...rest, }; } From b8c3d07ade9e564e0207da68c31918ffca9cc8d2 Mon Sep 17 00:00:00 2001 From: John Date: Wed, 3 Mar 2021 16:23:34 +0800 Subject: [PATCH 3/3] refactor: add test --- src/__tests__/load-more.test.tsx | 419 +++++++++++++++++++++++++++++++ src/useLoadMore.ts | 22 +- 2 files changed, 432 insertions(+), 9 deletions(-) create mode 100644 src/__tests__/load-more.test.tsx diff --git a/src/__tests__/load-more.test.tsx b/src/__tests__/load-more.test.tsx new file mode 100644 index 0000000..0b9f1cf --- /dev/null +++ b/src/__tests__/load-more.test.tsx @@ -0,0 +1,419 @@ +import { shallowMount } from '@vue/test-utils'; +import fetchMock from 'fetch-mock'; +import Mock from 'mockjs'; +import { defineComponent } from 'vue'; +import { clearGlobalOptions } from '../core/config'; +import { + FOCUS_LISTENER, + RECONNECT_LISTENER, + VISIBLE_LISTENER, +} from '../core/utils/listener'; +import { useLoadMore } from '../index'; +import { waitForTime } from './utils'; +import { failedRequest } from './utils/request'; + +type CustomPropertyMockDataType = { + myData: { + result: string[]; + }; +}; + +type NormalMockDataType = { + list: string[]; +}; + +describe('useLoadMore', () => { + beforeAll(() => { + jest.useFakeTimers('modern'); + }); + + const normalApi = 'http://example.com/normal'; + const customPropertyApi = 'http://example.com/custom'; + + // mock fetch + const normalMockData: NormalMockDataType = Mock.mock({ + 'list|10': ['@name'], + }); + + const customPropertyMockData: CustomPropertyMockDataType = { + myData: Mock.mock({ + 'result|10': ['@name'], + }), + }; + + fetchMock.get(normalApi, normalMockData, { delay: 1000 }); + fetchMock.get(customPropertyApi, customPropertyMockData, { delay: 1000 }); + + function generateNormalData(current: number, total = 10) { + let list: string[] = []; + if (current <= total) { + list = normalMockData.list; + } else { + list = []; + } + return { + current, + total, + list, + }; + } + + type NormalAPIReturnType = ReturnType; + type rawDataType = { + data: NormalAPIReturnType; + dataList: NormalAPIReturnType['list']; + }; + + const normalRequest = (rawData: rawDataType) => { + const current = (rawData?.data?.current || 0) + 1; + return new Promise(resolve => { + setTimeout(() => { + resolve(generateNormalData(current)); + }, 1000); + }); + }; + + function generateCustomData(current: number, total = 10) { + let list: string[] = []; + if (current <= total) { + list = customPropertyMockData.myData.result; + } else { + list = []; + } + return { + current, + total, + myData: { + result: list, + }, + }; + } + + type CustomAPIReturnType = ReturnType; + type CustomRawDataType = { + data: CustomAPIReturnType; + dataList: CustomAPIReturnType['myData']['result']; + }; + + const customRequest = (rawData: CustomRawDataType) => { + const current = (rawData?.data?.current || 0) + 1; + return new Promise(resolve => { + setTimeout(() => { + resolve(generateCustomData(current)); + }, 1000); + }); + }; + + const originalError = console.error; + beforeEach(() => { + console.error = jest.fn(); + // clear global options + clearGlobalOptions(); + + // clear listener + RECONNECT_LISTENER.clear(); + FOCUS_LISTENER.clear(); + VISIBLE_LISTENER.clear(); + }); + + afterEach(() => { + console.error = originalError; + }); + + test('should be defined', () => { + expect(useLoadMore).toBeDefined(); + }); + + test('useLoadMore only support function service', () => { + const fn = jest.fn(); + + try { + // @ts-ignore + useLoadMore(normalApi); + } catch (error) { + fn(); + expect(error.message).toBe('useLoadMore only support function service'); + } + + try { + // @ts-ignore + useLoadMore({ url: normalApi }); + } catch (error) { + fn(); + expect(error.message).toBe('useLoadMore only support function service'); + } + + expect(fn).toHaveBeenCalledTimes(2); + }); + + test('useLoadMore not support queryKey', () => { + const fn = jest.fn(); + + try { + useLoadMore(normalRequest, { + // @ts-ignore + queryKey: () => 'key', + }); + } catch (error) { + fn(); + expect(error.message).toBe( + 'useLoadMore does not support concurrent request', + ); + } + + expect(fn).toHaveBeenCalledTimes(1); + }); + + test('useLoadMore should work', async () => { + const wrapper = shallowMount( + defineComponent({ + setup() { + const { + dataList, + loadingMore, + loading, + noMore, + loadMore, + } = useLoadMore(normalRequest, { + isNoMore: d => d?.current >= d?.total, + }); + return () => ( +

+
{dataList.value.length || 0}
+
{`${loadingMore.value}`}
+
{`${loading.value}`}
+
{`${noMore.value}`}
+
{ + loadMore(); + }} + /> +
+ ); + }, + }), + ); + + const dataListEl = wrapper.find('.dataList'); + const loadingMoreEl = wrapper.find('.loadingMore'); + const loadingEl = wrapper.find('.loading'); + const loadMoreEl = wrapper.find('.loadMore'); + const noMoreEl = wrapper.find('.noMore'); + + expect(dataListEl.text()).toBe('0'); + expect(loadingMoreEl.text()).toBe('false'); + expect(loadingEl.text()).toBe('true'); + expect(noMoreEl.text()).toBe('false'); + + await waitForTime(1000); + expect(dataListEl.text()).toBe('10'); + expect(loadingMoreEl.text()).toBe('false'); + expect(loadingEl.text()).toBe('false'); + expect(noMoreEl.text()).toBe('false'); + + for (let index = 1; index <= 9; index++) { + await loadMoreEl.trigger('click'); + expect(loadingMoreEl.text()).toBe('true'); + expect(loadingEl.text()).toBe('true'); + await waitForTime(1000); + expect(dataListEl.text()).toBe(`${10 + index * 10}`); + expect(noMoreEl.text()).toBe(`${index === 9}`); + } + + for (let index = 0; index < 100; index++) { + await loadMoreEl.trigger('click'); + expect(loadingMoreEl.text()).toBe('false'); + expect(loadingEl.text()).toBe('false'); + await waitForTime(1000); + expect(loadingMoreEl.text()).toBe('false'); + expect(loadingEl.text()).toBe('false'); + expect(dataListEl.text()).toBe('100'); + expect(noMoreEl.text()).toBe('true'); + } + }); + + test('listKey should work', async () => { + const wrapper = shallowMount( + defineComponent({ + setup() { + const { + dataList, + loadingMore, + loading, + noMore, + loadMore, + } = useLoadMore(customRequest, { + isNoMore: d => d?.current >= d?.total, + listKey: 'myData.result', + }); + return () => ( +
+
{dataList.value.length || 0}
+
{`${loadingMore.value}`}
+
{`${loading.value}`}
+
{`${noMore.value}`}
+
{ + loadMore(); + }} + /> +
+ ); + }, + }), + ); + + const dataListEl = wrapper.find('.dataList'); + const loadingMoreEl = wrapper.find('.loadingMore'); + const loadingEl = wrapper.find('.loading'); + const loadMoreEl = wrapper.find('.loadMore'); + const noMoreEl = wrapper.find('.noMore'); + + expect(dataListEl.text()).toBe('0'); + expect(loadingMoreEl.text()).toBe('false'); + expect(loadingEl.text()).toBe('true'); + expect(noMoreEl.text()).toBe('false'); + + await waitForTime(1000); + expect(dataListEl.text()).toBe('10'); + expect(loadingMoreEl.text()).toBe('false'); + expect(loadingEl.text()).toBe('false'); + expect(noMoreEl.text()).toBe('false'); + + for (let index = 1; index <= 9; index++) { + await loadMoreEl.trigger('click'); + expect(loadingMoreEl.text()).toBe('true'); + expect(loadingEl.text()).toBe('true'); + await waitForTime(1000); + expect(dataListEl.text()).toBe(`${10 + index * 10}`); + expect(noMoreEl.text()).toBe(`${index === 9}`); + } + }); + + test('reload should work', async () => { + const wrapper = shallowMount( + defineComponent({ + setup() { + const { + dataList, + loadingMore, + noMore, + loading, + loadMore, + reload, + } = useLoadMore(normalRequest, { + isNoMore: d => d?.current >= d?.total, + }); + return () => ( +
+
{dataList.value.length || 0}
+
{`${loadingMore.value}`}
+
{`${loading.value}`}
+
{`${noMore.value}`}
+
{ + loadMore(); + }} + /> +
{ + reload(); + }} + /> +
+ ); + }, + }), + ); + + const dataListEl = wrapper.find('.dataList'); + const loadingMoreEl = wrapper.find('.loadingMore'); + const loadingEl = wrapper.find('.loading'); + const loadMoreEl = wrapper.find('.loadMore'); + const noMoreEl = wrapper.find('.noMore'); + const reloadEl = wrapper.find('.reload'); + + expect(loadingEl.text()).toBe('true'); + await waitForTime(1000); + expect(loadingEl.text()).toBe('false'); + expect(dataListEl.text()).toBe('10'); + expect(loadingMoreEl.text()).toBe('false'); + expect(noMoreEl.text()).toBe('false'); + + for (let index = 1; index <= 9; index++) { + await loadMoreEl.trigger('click'); + expect(loadingMoreEl.text()).toBe('true'); + expect(loadingEl.text()).toBe('true'); + await waitForTime(1000); + expect(loadingEl.text()).toBe('false'); + expect(loadingMoreEl.text()).toBe('false'); + expect(dataListEl.text()).toBe(`${10 + index * 10}`); + expect(noMoreEl.text()).toBe(`${index === 9}`); + } + + for (let index = 0; index < 100; index++) { + await loadMoreEl.trigger('click'); + expect(loadingMoreEl.text()).toBe('false'); + expect(loadingEl.text()).toBe('false'); + await waitForTime(1000); + expect(loadingEl.text()).toBe('false'); + expect(loadingMoreEl.text()).toBe('false'); + expect(dataListEl.text()).toBe('100'); + expect(noMoreEl.text()).toBe('true'); + } + + await reloadEl.trigger('click'); + expect(loadingEl.text()).toBe('true'); + expect(loadingMoreEl.text()).toBe('false'); + expect(dataListEl.text()).toBe('0'); + expect(noMoreEl.text()).toBe('false'); + await waitForTime(1000); + expect(loadingEl.text()).toBe('false'); + expect(dataListEl.text()).toBe('10'); + + for (let index = 1; index <= 9; index++) { + await loadMoreEl.trigger('click'); + expect(loadingEl.text()).toBe('true'); + expect(loadingMoreEl.text()).toBe('true'); + await waitForTime(1000); + expect(loadingEl.text()).toBe('false'); + expect(loadingMoreEl.text()).toBe('false'); + expect(dataListEl.text()).toBe(`${10 + index * 10}`); + expect(noMoreEl.text()).toBe(`${index === 9}`); + } + }); + + test('useLoadMore when request error', async () => { + const wrapper = shallowMount( + defineComponent({ + setup() { + const { loadingMore, loadMore } = useLoadMore(failedRequest); + return () => ( +
+
{`${loadingMore.value}`}
+
{ + loadMore(); + }} + /> +
+ ); + }, + }), + ); + + const loadingMoreEl = wrapper.find('.loadingMore'); + const loadMoreEl = wrapper.find('.loadMore'); + await waitForTime(1000); + expect(loadingMoreEl.text()).toBe('false'); + await loadMoreEl.trigger('click'); + expect(loadingMoreEl.text()).toBe('true'); + await waitForTime(1000); + expect(loadingMoreEl.text()).toBe('false'); + }); +}); diff --git a/src/useLoadMore.ts b/src/useLoadMore.ts index 616ab45..7f721ae 100644 --- a/src/useLoadMore.ts +++ b/src/useLoadMore.ts @@ -5,7 +5,6 @@ import get from 'lodash/get'; import generateService from './core/utils/generateService'; import { isFunction } from './core/utils'; import { ServiceParams } from './core/utils/types'; -import { Query } from './core/createQuery'; export interface LoadMoreResult extends Omit, 'queries'> { @@ -21,9 +20,9 @@ export interface LoadMoreExtendsOption { listKey?: string; } -export type LoadMoreService = - | ((r: { data: R; dataList: FR }, ...args: P) => Promise) - | ((r: { data: R; dataList: FR }, ...args: P) => ServiceParams); +export type LoadMoreService = + | ((r: { data: R; dataList: LR }, ...args: P) => Promise) + | ((r: { data: R; dataList: LR }, ...args: P) => ServiceParams); export type LoadMoreFormatOptions = Omit< FormatOptions, @@ -92,10 +91,14 @@ function useLoadMore( increaseQueryKey.value++; restOptions?.onSuccess?.(...p); }, + onError: (...p) => { + loadingMore.value = false; + restOptions?.onError?.(...p); + }, queryKey: () => String(increaseQueryKey.value), }); - const latestData = ref(data.value) as Ref; + const latestData = >ref(data.value); watchEffect(() => { if (data.value !== undefined) { latestData.value = data.value; @@ -103,14 +106,15 @@ function useLoadMore( }); const noMore = computed(() => { - return isNoMore ? isNoMore(latestData.value) : false; + return isNoMore && isFunction(isNoMore) + ? isNoMore(latestData.value) + : false; }); const dataList = computed(() => { let list: any[] = []; Object.values(queries).forEach(h => { - const queriesData = h.data as any; - const dataList = get(queriesData.value, listKey); + const dataList = get(h.data, listKey); if (dataList && Array.isArray(dataList)) { list = list.concat(dataList); } @@ -134,13 +138,13 @@ function useLoadMore( const reload = () => { reset(); increaseQueryKey.value = 0; + latestData.value = undefined; const [, ...restParams] = params.value; const mergerParams = [undefined, ...restParams] as any; run(...mergerParams); }; return { - // 每次 LoadMore 触发时,data 都会变成undefined,原因是 queries data: latestData, dataList: dataList, params,