Skip to content

Commit

Permalink
feat(useSize): support debounceOptions and throttleOptions(#2647)
Browse files Browse the repository at this point in the history
  • Loading branch information
tchen committed Oct 8, 2024
1 parent 299fa2a commit 0282530
Show file tree
Hide file tree
Showing 7 changed files with 11,728 additions and 12,773 deletions.
115 changes: 99 additions & 16 deletions packages/hooks/src/useSize/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { useRef } from 'react';
import { renderHook, act, render, screen } from '@testing-library/react';
import useSize from '../index';
import React, { useRef, act } from "react";
import { renderHook, render, screen } from "@testing-library/react";
import useSize from "../index";
import { sleep } from "../../utils/testingHelpers";

let callback;
jest.mock('resize-observer-polyfill', () => {
jest.mock("resize-observer-polyfill", () => {
return jest.fn().mockImplementation((cb) => {
callback = cb;
return {
Expand All @@ -14,15 +15,15 @@ jest.mock('resize-observer-polyfill', () => {
});

// test about Resize Observer see https://github.com/que-etc/resize-observer-polyfill/tree/master/tests
describe('useSize', () => {
it('should work when target is a mounted DOM', () => {
describe("useSize", () => {
it("should work when target is a mounted DOM", () => {
const hook = renderHook(() => useSize(document.body));
expect(hook.result.current).toEqual({ height: 0, width: 0 });
});

it('should work when target is a `MutableRefObject`', async () => {
it("should work when target is a `MutableRefObject`", async () => {
const mockRaf = jest
.spyOn(window, 'requestAnimationFrame')
.spyOn(window, "requestAnimationFrame")
.mockImplementation((cb: FrameRequestCallback) => {
cb(0);
return 0;
Expand All @@ -41,29 +42,33 @@ describe('useSize', () => {
}

render(<Setup />);
expect(await screen.findByText(/^width/)).toHaveTextContent('width: undefined');
expect(await screen.findByText(/^height/)).toHaveTextContent('height: undefined');
expect(await screen.findByText(/^width/)).toHaveTextContent(
"width: undefined"
);
expect(await screen.findByText(/^height/)).toHaveTextContent(
"height: undefined"
);

act(() => callback([{ target: { clientWidth: 10, clientHeight: 10 } }]));
expect(await screen.findByText(/^width/)).toHaveTextContent('width: 10');
expect(await screen.findByText(/^height/)).toHaveTextContent('height: 10');
expect(await screen.findByText(/^width/)).toHaveTextContent("width: 10");
expect(await screen.findByText(/^height/)).toHaveTextContent("height: 10");
mockRaf.mockRestore();
});

it('should not work when target is null', () => {
it("should not work when target is null", () => {
expect(() => {
renderHook(() => useSize(null));
}).not.toThrowError();
});

it('should work', () => {
it("should work", () => {
const mockRaf = jest
.spyOn(window, 'requestAnimationFrame')
.spyOn(window, "requestAnimationFrame")
.mockImplementation((cb: FrameRequestCallback) => {
cb(0);
return 0;
});
const targetEl = document.createElement('div');
const targetEl = document.createElement("div");
const { result } = renderHook(() => useSize(targetEl));

act(() => {
Expand All @@ -84,4 +89,82 @@ describe('useSize', () => {

mockRaf.mockRestore();
});

it("debounceOptions should work", async () => {
let count = 0;

function Setup() {
const ref = useRef(null);
const size = useSize(ref, { debounceOptions: { wait: 200 } });
count += 1;

return (
<div ref={ref}>
<div>width: {String(size?.width)}</div>
<div>height: {String(size?.height)}</div>
</div>
);
}

render(<Setup />);

act(() => callback([{ target: { clientWidth: 10, clientHeight: 10 } }]));
act(() => callback([{ target: { clientWidth: 20, clientHeight: 20 } }]));
act(() => callback([{ target: { clientWidth: 30, clientHeight: 30 } }]));

expect(count).toBe(1);
expect(await screen.findByText(/^width/)).toHaveTextContent(
"width: undefined"
);
expect(await screen.findByText(/^height/)).toHaveTextContent(
"height: undefined"
);

await sleep(300);

expect(count).toBe(2);
expect(await screen.findByText(/^width/)).toHaveTextContent("width: 30");
expect(await screen.findByText(/^height/)).toHaveTextContent("height: 30");
});

it("throttleOptions should work", async () => {
let count = 0;

function Setup() {
const ref = useRef(null);
const size = useSize(ref, { throttleOptions: { wait: 500 } });
count += 1;

return (
<div ref={ref}>
<div>width: {String(size?.width)}</div>
<div>height: {String(size?.height)}</div>
</div>
);
}

render(<Setup />);

act(() => callback([{ target: { clientWidth: 10, clientHeight: 10 } }]));
act(() => callback([{ target: { clientWidth: 20, clientHeight: 20 } }]));
act(() => callback([{ target: { clientWidth: 30, clientHeight: 30 } }]));

expect(count).toBe(1);
expect(await screen.findByText(/^width/)).toHaveTextContent(
"width: undefined"
);
expect(await screen.findByText(/^height/)).toHaveTextContent(
"height: undefined"
);

await sleep(450);
expect(count).toBe(2);
expect(await screen.findByText(/^width/)).toHaveTextContent("width: 10");
expect(await screen.findByText(/^height/)).toHaveTextContent("height: 10");

await sleep(200);
expect(count).toBe(3);
expect(await screen.findByText(/^width/)).toHaveTextContent("width: 30");
expect(await screen.findByText(/^height/)).toHaveTextContent("height: 30");
});
});
23 changes: 23 additions & 0 deletions packages/hooks/src/useSize/demo/demo3.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* title: debounce
* desc: useSize can receive debounceOptions as argument
*
* title.zh-CN: 防抖
* desc.zh-CN: useSize 可以接收 debounceOptions 参数
*/

import React, { useRef } from "react";
import { useSize } from "ahooks";

export default () => {
const ref = useRef(null);
const size = useSize(ref, { debounceOptions: { wait: 300 } });
return (
<div ref={ref}>
<p>Try to resize the preview window </p>
<p>
width: {size?.width}px, height: {size?.height}px
</p>
</div>
);
};
23 changes: 23 additions & 0 deletions packages/hooks/src/useSize/demo/demo4.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* title: throttle
* desc: useSize can receive throttleOptions as argument
*
* title.zh-CN: 节流
* desc.zh-CN: useSize 可以接收 throttleOptions 参数
*/

import React, { useRef } from "react";
import { useSize } from "ahooks";

export default () => {
const ref = useRef(null);
const size = useSize(ref, { throttleOptions: { wait: 300 } });
return (
<div ref={ref}>
<p>Try to resize the preview window </p>
<p>
width: {size?.width}px, height: {size?.height}px
</p>
</div>
);
};
21 changes: 17 additions & 4 deletions packages/hooks/src/useSize/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,30 @@ A hook that observes size change of an element.

<code src="./demo/demo2.tsx" />

### debounce

<code src="./demo/demo3.tsx" />

### throttle

<code src="./demo/demo4.tsx" />

## API

```typescript
const size = useSize(target);
const size = useSize(target, options?: {
debounceOptions?: DebounceOptions,
throttleOptions?: ThrottleOptions,
});
```

### Params

| Property | Description | Type | Default |
| -------- | ------------------------- | ------------------------------------------------------------- | ------- |
| target | DOM element or ref object | `Element` \| `(() => Element)` \| `MutableRefObject<Element>` | - |
| Property | Description | Type | Default |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------- |
| target | DOM element or ref object | `Element` \| `(() => Element)` \| `MutableRefObject<Element>` | - |
| options.debounceOptions | debounce options(same as useDebounce) | `DebounceOptions` | - |
| options.throttleOptions | throttle options(same as useThrottle), when debounceOptions exists at the same time, debounceOptions is used first. | `ThrottleOptions` | - |

### Result

Expand Down
55 changes: 41 additions & 14 deletions packages/hooks/src/useSize/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,45 @@
import ResizeObserver from 'resize-observer-polyfill';
import useRafState from '../useRafState';
import type { BasicTarget } from '../utils/domTarget';
import { getTargetElement } from '../utils/domTarget';
import useIsomorphicLayoutEffectWithTarget from '../utils/useIsomorphicLayoutEffectWithTarget';
import ResizeObserver from "resize-observer-polyfill";
import useRafState from "../useRafState";
import type { BasicTarget } from "../utils/domTarget";
import { getTargetElement } from "../utils/domTarget";
import useIsomorphicLayoutEffectWithTarget from "../utils/useIsomorphicLayoutEffectWithTarget";
import useDebounceFn from "../useDebounceFn";
import type { DebounceOptions } from "../useDebounce/debounceOptions";
import useMemoizedFn from "../useMemoizedFn";
import useThrottleFn from "../useThrottleFn";
import type { ThrottleOptions } from "../useThrottle/throttleOptions";

type Size = { width: number; height: number };

function useSize(target: BasicTarget): Size | undefined {
const [state, setState] = useRafState<Size | undefined>(
() => {
const el = getTargetElement(target);
return el ? { width: el.clientWidth, height: el.clientHeight } : undefined
},
);
function useSize(
target: BasicTarget,
options?: {
debounceOptions?: DebounceOptions;
throttleOptions?: ThrottleOptions;
}
): Size | undefined {
const [state, setState] = useRafState<Size | undefined>(() => {
const el = getTargetElement(target);
return el ? { width: el.clientWidth, height: el.clientHeight } : undefined;
});

const debounce = useDebounceFn(setState, options?.debounceOptions);

const throttle = useThrottleFn(setState, options?.throttleOptions);

const setStateMemoizedFn = useMemoizedFn((nextState: Size) => {
if (options?.debounceOptions) {
debounce.run(nextState);
return;
}

if (options?.throttleOptions) {
throttle.run(nextState);
return;
}

setState(nextState);
});

useIsomorphicLayoutEffectWithTarget(
() => {
Expand All @@ -25,7 +52,7 @@ function useSize(target: BasicTarget): Size | undefined {
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const { clientWidth, clientHeight } = entry.target;
setState({ width: clientWidth, height: clientHeight });
setStateMemoizedFn({ width: clientWidth, height: clientHeight });
});
});
resizeObserver.observe(el);
Expand All @@ -34,7 +61,7 @@ function useSize(target: BasicTarget): Size | undefined {
};
},
[],
target,
target
);

return state;
Expand Down
21 changes: 17 additions & 4 deletions packages/hooks/src/useSize/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,30 @@ nav:

<code src="./demo/demo2.tsx" />

### 防抖

<code src="./demo/demo3.tsx" />

### 节流

<code src="./demo/demo4.tsx" />

## API

```typescript
const size = useSize(target);
const size = useSize(target, options?: {
debounceOptions?: DebounceOptions,
throttleOptions?: ThrottleOptions,
});
```

### Params

| 参数 | 说明 | 类型 | 默认值 |
| ------ | ---------------- | ------------------------------------------------------------- | ------ |
| target | DOM 节点或者 ref | `Element` \| `(() => Element)` \| `MutableRefObject<Element>` | - |
| 参数 | 说明 | 类型 | 默认值 |
| ----------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------ |
| target | DOM 节点或者 ref | `Element` \| `(() => Element)` \| `MutableRefObject<Element>` | - |
| options.debounceOptions | 防抖参数(同 useDebounce) | `DebounceOptions` | - |
| options.throttleOptions | 节流参数(同 useThrottle),与 debounceOptions 同时存在时,优先使用 debounceOptions | `ThrottleOptions` | - |

### Result

Expand Down
Loading

0 comments on commit 0282530

Please sign in to comment.