Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(useSize): support debounceOptions and throttleOptions(#2647) #2655

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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