From 88350fc4df04be657380876ae842d77ea97284e1 Mon Sep 17 00:00:00 2001 From: guaijie <30885718+guaijie@users.noreply.github.com> Date: Mon, 20 Jan 2025 01:52:56 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E5=88=B0=E8=A7=86=E7=AA=97=E5=90=8E=E5=86=8D=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/useAutoRunPlugin.test.ts | 77 ++++++++++++++++++- .../src/plugins/useAutoRunPlugin.ts | 38 ++++++--- packages/hooks/src/useRequest/src/types.ts | 15 +++- 3 files changed, 116 insertions(+), 14 deletions(-) diff --git a/packages/hooks/src/useRequest/__tests__/useAutoRunPlugin.test.ts b/packages/hooks/src/useRequest/__tests__/useAutoRunPlugin.test.ts index ea40d5012c..25716b6b7e 100644 --- a/packages/hooks/src/useRequest/__tests__/useAutoRunPlugin.test.ts +++ b/packages/hooks/src/useRequest/__tests__/useAutoRunPlugin.test.ts @@ -1,6 +1,16 @@ import { act, renderHook, waitFor } from '@testing-library/react'; -import useRequest from '../index'; import { request } from '../../utils/testingHelpers'; +import useRequest from '../index'; + +const targetEl = document.createElement('div'); +document.body.appendChild(targetEl); + +const mockIntersectionObserver = jest.fn().mockReturnValue({ + observe: jest.fn(), + disconnect: jest.fn, +}); + +window.IntersectionObserver = mockIntersectionObserver; describe('useAutoRunPlugin', () => { jest.useFakeTimers(); @@ -284,4 +294,69 @@ describe('useAutoRunPlugin', () => { expect(hook.result.current.params).toEqual([2]); expect(fn).toHaveBeenCalledTimes(1); }); + + it('should work when target is in viewport', async () => { + const obj = { request }; + + const mockRequest = jest.spyOn(obj, 'request'); + + hook = setUp(obj.request, { + target: targetEl, + }); + + const calls = mockIntersectionObserver.mock.calls; + const [onChange] = calls[calls.length - 1]; + + expect(mockRequest).toHaveBeenCalledTimes(0); + act(() => onChange([{ isIntersecting: true }])); + expect(mockRequest).toHaveBeenCalledTimes(1); + }); + + it('should work once when target is in viewport', async () => { + const obj = { request }; + + const mockRequest = jest.spyOn(obj, 'request'); + + hook = setUp(obj.request, { + target: targetEl, + }); + + const calls = mockIntersectionObserver.mock.calls; + const [onChange] = calls[calls.length - 1]; + + act(() => onChange([{ isIntersecting: true }])); + act(() => onChange([{ isIntersecting: false }])); + act(() => onChange([{ isIntersecting: true }])); + expect(mockRequest).toHaveBeenCalledTimes(1); + }); + + it('should work when target is in viewport and refreshDeps changed', async () => { + let dep = 1; + + const obj = { request }; + + const mockRequest = jest.spyOn(obj, 'request'); + + hook = setUp(obj.request, { + refreshDeps: [dep], + target: targetEl, + }); + + const calls = mockIntersectionObserver.mock.calls; + const [onChange] = calls[calls.length - 1]; + + act(() => onChange([{ isIntersecting: true }])); + act(() => onChange([{ isIntersecting: false }])); + expect(mockRequest).toHaveBeenCalledTimes(1); + + dep = 2; + hook.rerender({ + refreshDeps: [dep], + target: targetEl, + }); + + expect(mockRequest).toHaveBeenCalledTimes(1); + act(() => onChange([{ isIntersecting: true }])); + expect(mockRequest).toHaveBeenCalledTimes(2); + }); }); diff --git a/packages/hooks/src/useRequest/src/plugins/useAutoRunPlugin.ts b/packages/hooks/src/useRequest/src/plugins/useAutoRunPlugin.ts index 7f852d7ca8..c17a4028d6 100644 --- a/packages/hooks/src/useRequest/src/plugins/useAutoRunPlugin.ts +++ b/packages/hooks/src/useRequest/src/plugins/useAutoRunPlugin.ts @@ -1,43 +1,63 @@ import { useRef } from 'react'; +import useInViewport from '../../../useInViewport'; import useUpdateEffect from '../../../useUpdateEffect'; import type { Plugin } from '../types'; // support refreshDeps & ready const useAutoRunPlugin: Plugin = ( fetchInstance, - { manual, ready = true, defaultParams = [], refreshDeps = [], refreshDepsAction }, + { + manual, + ready = true, + defaultParams = [], + refreshDeps = [], + target, + root, + rootMargin, + threshold, + refreshDepsAction, + }, ) => { const hasAutoRun = useRef(false); hasAutoRun.current = false; + const shouldRun = useRef(true); + let [visible] = useInViewport(target, { root, rootMargin, threshold }); + if (!target) visible = true; + + useUpdateEffect(() => { + shouldRun.current = ready; + }, [ready, ...refreshDeps]); + useUpdateEffect(() => { - if (!manual && ready) { + if (!manual && ready && visible && shouldRun.current) { hasAutoRun.current = true; + shouldRun.current = false; fetchInstance.run(...defaultParams); } - }, [ready]); + }, [ready, visible]); useUpdateEffect(() => { if (hasAutoRun.current) { return; } - if (!manual) { + if (!manual && visible && shouldRun.current) { hasAutoRun.current = true; + shouldRun.current = false; if (refreshDepsAction) { refreshDepsAction(); } else { fetchInstance.refresh(); } } - }, [...refreshDeps]); + }, [...refreshDeps, visible]); return { onBefore: () => { - if (!ready) { - return { - stopNow: true, - }; + if (target) { + return { stopNow: shouldRun.current || !ready }; } + return { stopNow: !ready }; }, }; }; diff --git a/packages/hooks/src/useRequest/src/types.ts b/packages/hooks/src/useRequest/src/types.ts index 442a2dff73..da5d39e471 100644 --- a/packages/hooks/src/useRequest/src/types.ts +++ b/packages/hooks/src/useRequest/src/types.ts @@ -1,4 +1,5 @@ import type { DependencyList } from 'react'; +import type { BasicTarget } from '../../utils/domTarget'; import type Fetch from './Fetch'; import type { CachedData } from './utils/cache'; @@ -87,6 +88,12 @@ export interface Options { retryCount?: number; retryInterval?: number; + // viewport + target?: BasicTarget | BasicTarget[]; + root?: BasicTarget; + rootMargin?: string; + threshold?: number | number[]; + // ready ready?: boolean; @@ -94,10 +101,10 @@ export interface Options { } export type Plugin = { - (fetchInstance: Fetch, options: Options): PluginReturn< - TData, - TParams - >; + ( + fetchInstance: Fetch, + options: Options, + ): PluginReturn; onInit?: (options: Options) => Partial>; };