diff --git a/packages/hooks/src/useRequest/__tests__/usePollingPlugin.test.ts b/packages/hooks/src/useRequest/__tests__/usePollingPlugin.test.ts index b7a01c4e9f..a067b85452 100644 --- a/packages/hooks/src/useRequest/__tests__/usePollingPlugin.test.ts +++ b/packages/hooks/src/useRequest/__tests__/usePollingPlugin.test.ts @@ -116,4 +116,79 @@ describe('usePollingPlugin', () => { }); await waitFor(() => expect(errorCallback).toHaveBeenCalledTimes(5)); }); + + it('usePollingPlugin pollingInterval=100 pollingWhenHidden=true should return pollingLoading', async () => { + let hook; + const callback = jest.fn(); + act(() => { + hook = setUp( + () => { + callback(); + return request(1); + }, + { + pollingInterval: 100, + pollingWhenHidden: true, + }, + ); + }); + expect(hook.result.current.loading).toBe(true); + // Initial state: pollingLoading should be false + expect(hook.result.current.pollingLoading).toBe(false); + + // first request completed + act(() => { + jest.runAllTimers(); + }); + await waitFor(() => expect(hook.result.current.loading).toBe(false)); + expect(callback).toHaveBeenCalledTimes(1); + + // trigger the next polling request + act(() => { + jest.runAllTimers(); + }); + expect(hook.result.current.loading).toBe(true); + // pollingLoading: true => false + expect(hook.result.current.pollingLoading).toBe(true); + await waitFor(() => expect(hook.result.current.pollingLoading).toBe(false)); + expect(hook.result.current.data).toBe('success'); + expect(callback).toHaveBeenCalledTimes(2); + + act(() => { + jest.runAllTimers(); + }); + // pollingLoading: true => false, synchronized with loading + expect(hook.result.current.loading).toBe(true); + expect(hook.result.current.pollingLoading).toBe(true); + await waitFor(() => expect(hook.result.current.loading).toBe(false)); + expect(hook.result.current.pollingLoading).toBe(false); + expect(hook.result.current.data).toBe('success'); + expect(callback).toHaveBeenCalledTimes(3); + + act(() => { + hook.result.current.cancel(); + }); + expect(hook.result.current.pollingLoading).toBe(false); + + // should be false in manual run + act(() => { + hook.result.current.run(); + }); + expect(hook.result.current.loading).toBe(true); + expect(hook.result.current.pollingLoading).toBe(false); + await waitFor(() => expect(callback).toHaveBeenCalledTimes(4)); + + // first request completed + act(() => { + jest.runAllTimers(); + }); + await waitFor(() => expect(hook.result.current.loading).toBe(false)); + + act(() => { + jest.runAllTimers(); + }); + // pollingLoading: true => false + expect(hook.result.current.pollingLoading).toBe(true); + await waitFor(() => expect(hook.result.current.pollingLoading).toBe(false)); + }); }); diff --git a/packages/hooks/src/useRequest/doc/polling/demo/polling.tsx b/packages/hooks/src/useRequest/doc/polling/demo/polling.tsx index dcf8f0facb..02a5bcaa3f 100644 --- a/packages/hooks/src/useRequest/doc/polling/demo/polling.tsx +++ b/packages/hooks/src/useRequest/doc/polling/demo/polling.tsx @@ -1,5 +1,5 @@ import { useRequest } from 'ahooks'; -import React from 'react'; +import React, { useState } from 'react'; import Mock from 'mockjs'; function getUsername() { @@ -12,14 +12,14 @@ function getUsername() { } export default () => { - const { data, loading, run, cancel } = useRequest(getUsername, { + const { data, loading, run, cancel, pollingLoading } = useRequest(getUsername, { pollingInterval: 1000, pollingWhenHidden: false, }); return ( <> -

Username: {loading ? 'Loading' : data}

+

Username: {pollingLoading ? 'PollingLoading' : loading ? 'Loading' : data}

diff --git a/packages/hooks/src/useRequest/doc/polling/polling.en-US.md b/packages/hooks/src/useRequest/doc/polling/polling.en-US.md index 56c69db578..3e70feda74 100644 --- a/packages/hooks/src/useRequest/doc/polling/polling.en-US.md +++ b/packages/hooks/src/useRequest/doc/polling/polling.en-US.md @@ -40,11 +40,12 @@ You can experience the effect through the following example. ### Return -| Property | Description | Type | -| -------- | ------------- | ---------------------------------------- | -| run | Start polling | `(...params: TParams) => void` | -| runAsync | Start polling | `(...params: TParams) => Promise` | -| cancel | Stop polling | `() => void` | +| Property | Description | Type | +| -------------- | ---------------------------------- | ---------------------------------------- | +| PollingLoading | In the polling request in progress | `boolean` | +| run | Start polling | `(...params: TParams) => void` | +| runAsync | Start polling | `(...params: TParams) => Promise` | +| cancel | Stop polling | `() => void` | ### Options diff --git a/packages/hooks/src/useRequest/doc/polling/polling.zh-CN.md b/packages/hooks/src/useRequest/doc/polling/polling.zh-CN.md index b124f49d02..8e6aa4f00d 100644 --- a/packages/hooks/src/useRequest/doc/polling/polling.zh-CN.md +++ b/packages/hooks/src/useRequest/doc/polling/polling.zh-CN.md @@ -40,11 +40,12 @@ const { data, run, cancel } = useRequest(getUsername, { ### Return -| 参数 | 说明 | 类型 | -| -------- | -------- | ---------------------------------------- | -| run | 启动轮询 | `(...params: TParams) => void` | -| runAsync | 启动轮询 | `(...params: TParams) => Promise` | -| cancel | 停止轮询 | `() => void` | +| 参数 | 说明 | 类型 | +| -------------- | -------------------- | ---------------------------------------- | +| PollingLoading | 是否正在进行轮询请求 | `boolean` | +| run | 启动轮询 | `(...params: TParams) => void` | +| runAsync | 启动轮询 | `(...params: TParams) => Promise` | +| cancel | 停止轮询 | `() => void` | ### Options diff --git a/packages/hooks/src/useRequest/src/Fetch.ts b/packages/hooks/src/useRequest/src/Fetch.ts index 73ceee6004..5a4357bbe4 100644 --- a/packages/hooks/src/useRequest/src/Fetch.ts +++ b/packages/hooks/src/useRequest/src/Fetch.ts @@ -99,7 +99,8 @@ export default class Fetch { this.options.onFinally?.(params, res, undefined); if (currentCount === this.count) { - this.runPluginHandler('onFinally', params, res, undefined); + const state = this.runPluginHandler('onFinally', params, res, undefined); + this.setState({ ...state }); } return res; @@ -120,7 +121,8 @@ export default class Fetch { this.options.onFinally?.(params, undefined, error); if (currentCount === this.count) { - this.runPluginHandler('onFinally', params, undefined, error); + const state = this.runPluginHandler('onFinally', params, undefined, error); + this.setState({ ...state }); } throw error; diff --git a/packages/hooks/src/useRequest/src/plugins/usePollingPlugin.ts b/packages/hooks/src/useRequest/src/plugins/usePollingPlugin.ts index bf9a30b47c..9596ff05d7 100644 --- a/packages/hooks/src/useRequest/src/plugins/usePollingPlugin.ts +++ b/packages/hooks/src/useRequest/src/plugins/usePollingPlugin.ts @@ -1,4 +1,4 @@ -import { useRef } from 'react'; +import { useRef, useState } from 'react'; import useUpdateEffect from '../../../useUpdateEffect'; import type { Plugin, Timeout } from '../types'; import isDocumentVisible from '../utils/isDocumentVisible'; @@ -11,6 +11,7 @@ const usePollingPlugin: Plugin = ( const timerRef = useRef(); const unsubscribeRef = useRef<() => void>(); const countRef = useRef(0); + const pollingLoadingRef = useRef(false); const stopPolling = () => { if (timerRef.current) { @@ -32,12 +33,17 @@ const usePollingPlugin: Plugin = ( return { onBefore: () => { stopPolling(); + return { + pollingLoading: pollingLoadingRef.current, + }; }, onError: () => { countRef.current += 1; + pollingLoadingRef.current = false; }, onSuccess: () => { countRef.current = 0; + pollingLoadingRef.current = false; }, onFinally: () => { if ( @@ -49,18 +55,24 @@ const usePollingPlugin: Plugin = ( // if pollingWhenHidden = false && document is hidden, then stop polling and subscribe revisible if (!pollingWhenHidden && !isDocumentVisible()) { unsubscribeRef.current = subscribeReVisible(() => { + pollingLoadingRef.current = true; fetchInstance.refresh(); }); } else { + pollingLoadingRef.current = true; fetchInstance.refresh(); } }, pollingInterval); } else { countRef.current = 0; } + return { + pollingLoading: pollingLoadingRef.current, + }; }, onCancel: () => { stopPolling(); + pollingLoadingRef.current = false; }, }; }; diff --git a/packages/hooks/src/useRequest/src/types.ts b/packages/hooks/src/useRequest/src/types.ts index 442a2dff73..5034185087 100644 --- a/packages/hooks/src/useRequest/src/types.ts +++ b/packages/hooks/src/useRequest/src/types.ts @@ -12,6 +12,7 @@ export interface FetchState { params?: TParams; data?: TData; error?: Error; + pollingLoading?: boolean; } export interface PluginReturn { @@ -94,10 +95,10 @@ export interface Options { } export type Plugin = { - (fetchInstance: Fetch, options: Options): PluginReturn< - TData, - TParams - >; + ( + fetchInstance: Fetch, + options: Options, + ): PluginReturn; onInit?: (options: Options) => Partial>; }; @@ -119,6 +120,7 @@ export interface Result { run: Fetch['run']; runAsync: Fetch['runAsync']; mutate: Fetch['mutate']; + pollingLoading?: boolean; } export type Timeout = ReturnType; diff --git a/packages/hooks/src/useRequest/src/useRequestImplement.ts b/packages/hooks/src/useRequest/src/useRequestImplement.ts index d65cfba6cb..2defec9c15 100644 --- a/packages/hooks/src/useRequest/src/useRequestImplement.ts +++ b/packages/hooks/src/useRequest/src/useRequestImplement.ts @@ -64,6 +64,7 @@ function useRequestImplement( data: fetchInstance.state.data, error: fetchInstance.state.error, params: fetchInstance.state.params || [], + pollingLoading: fetchInstance.state.pollingLoading, cancel: useMemoizedFn(fetchInstance.cancel.bind(fetchInstance)), refresh: useMemoizedFn(fetchInstance.refresh.bind(fetchInstance)), refreshAsync: useMemoizedFn(fetchInstance.refreshAsync.bind(fetchInstance)),