Skip to content

Commit

Permalink
chore: merge main branch to v4 (#2528)
Browse files Browse the repository at this point in the history
* refactor: replace lodash/isEqual with react-fast-compare (#2458)

* refactor: replace lodash/isEqual with react-fast-compare

* chore: update lock

---------

Co-authored-by: 云泥 <1656081615@qq.com>

* refactor(useRafInterval): optimize useEffect cleanup fn (#2426)

* fix(useAntdTable): 修复动态设置 ready 失效的 bug (#2489)

* refactor(useLockFn): catch to finally (#2421)

* refactor(useLockFn): catch to finally

* refactor(useLockFn): keep the throw e

* refactor(createEffectWithTarget): the first parameter is old-deps (#2431)

* fix(useCountDown): targetDate resets leftTime (#2346)

* chore: update version

* fix(type): fixed up some types that are non-standard and errors on `useExteneral` hook (#2508)

* fix(type): fixed up some types that are non-standard and errors on `useExternal` hook

* style: format

* refactor: simplify

---------

Co-authored-by: liuyib <1656081615@qq.com>

* feat(useStoage): listen to `storage` event (#2298)

* feat: sync `storage` state

* chore: update test case

* refactor: replace StorageEvent to CustomEvent, and add listenStorageChange param

* test: revert case in useSessionStorage

* docs: modify docs and demo

* test: add case for 'enable' parameter

* test: remove unnecessary code

---------

Co-authored-by: liuyib <1656081615@qq.com>

* chore(useLatest): add a default state for comparison in useLatest demo (#2523)

* feat(useSelections): support object array (#2485)

* feat(useSelections): support object array

* fix: solve error

* 修复 useAntdTable Params 的类型问题 (#2377)

* fix(useAntdTable): change Params filter to filters

* docs(useAntdTable): change Params filter to filters

* fix(useVirtualList): get correct offset when itemHeight is fixed (#2279)

Co-authored-by: huangcheng <huangcheng.lq@bytedance.com>

---------

Co-authored-by: Guo Yunhe <i@guoyunhe.me>
Co-authored-by: ice <49827327+GetWebHB@users.noreply.github.com>
Co-authored-by: guaijie <30885718+guaijie@users.noreply.github.com>
Co-authored-by: joe-leong <39672163+joe-leong@users.noreply.github.com>
Co-authored-by: 潇见 <xiaojian.lj@antgroup.com>
Co-authored-by: Darwish <38754760+LonelyFellas@users.noreply.github.com>
Co-authored-by: vaakian <vaakian@gmail.com>
Co-authored-by: shanyue <xianger94@gmail.com>
Co-authored-by: Conor <729354837@qq.com>
Co-authored-by: echo <1558449520@qq.com>
Co-authored-by: huangcheng <huangcheng.lq@bytedance.com>
  • Loading branch information
12 people committed Apr 22, 2024
1 parent 9e14694 commit 7340355
Show file tree
Hide file tree
Showing 23 changed files with 660 additions and 148 deletions.
60 changes: 53 additions & 7 deletions packages/hooks/src/createUseStorageState/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { useState } from 'react';
import useEventListener from '../useEventListener';
import useMemoizedFn from '../useMemoizedFn';
import useUpdateEffect from '../useUpdateEffect';
import { isFunction, isUndef } from '../utils';

export const SYNC_STORAGE_EVENT_NAME = 'AHOOKS_SYNC_STORAGE_EVENT_NAME';

export type SetState<S> = S | ((prevState?: S) => S);

export interface Options<T> {
defaultValue?: T | (() => T);
listenStorageChange?: boolean;
serializer?: (value: T) => string;
deserializer?: (value: string) => T;
onError?: (error: unknown) => void;
Expand All @@ -16,6 +20,7 @@ export function createUseStorageState(getStorage: () => Storage | undefined) {
function useStorageState<T>(key: string, options: Options<T> = {}) {
let storage: Storage | undefined;
const {
listenStorageChange = false,
onError = (e) => {
console.error(e);
},
Expand Down Expand Up @@ -67,17 +72,58 @@ export function createUseStorageState(getStorage: () => Storage | undefined) {
const currentState = isFunction(value) ? value(state) : value;
setState(currentState);

if (isUndef(currentState)) {
storage?.removeItem(key);
} else {
try {
storage?.setItem(key, serializer(currentState));
} catch (e) {
console.error(e);
try {
let newValue: string | null;
const oldValue = storage?.getItem(key);

if (isUndef(currentState)) {
newValue = null;
storage?.removeItem(key);
} else {
newValue = serializer(currentState);
storage?.setItem(key, newValue);
}

dispatchEvent(
// send custom event to communicate within same page
// importantly this should not be a StorageEvent since those cannot
// be constructed with a non-built-in storage area
new CustomEvent(SYNC_STORAGE_EVENT_NAME, {
detail: {
key,
newValue,
oldValue,
storageArea: storage,
},
}),
);
} catch (e) {
onError(e);
}
};

const syncState = (event: StorageEvent) => {
if (event.key !== key || event.storageArea !== storage) {
return;
}

setState(getStoredValue());
};

const syncStateFromCustomEvent = (event: CustomEvent<StorageEvent>) => {
syncState(event.detail);
};

// from another document
useEventListener('storage', syncState, {
enable: listenStorageChange,
});

// from the same document but different hooks
useEventListener(SYNC_STORAGE_EVENT_NAME, syncStateFromCustomEvent, {
enable: listenStorageChange,
});

return [state, useMemoizedFn(updateState)] as const;
}
return useStorageState;
Expand Down
2 changes: 1 addition & 1 deletion packages/hooks/src/useAntdTable/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ All parameters and returned results of `useRequest` are applicable to `useAntdTa
```typescript

type Data = { total: number; list: any[] };
type Params = [{ current: number; pageSize: number, filter?: any, sorter?: any, extra?: any }, { [key: string]: any }];
type Params = [{ current: number; pageSize: number, filters?: any, sorter?: any, extra?: any }, { [key: string]: any }];

const {
...,
Expand Down
2 changes: 1 addition & 1 deletion packages/hooks/src/useAntdTable/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ demo:
```typescript

type Data = { total: number; list: any[] };
type Params = [{ current: number; pageSize: number, filter?: any, sorter?: any, extra?: any }, { [key: string]: any }];
type Params = [{ current: number; pageSize: number, filters?: any, sorter?: any, extra?: any }, { [key: string]: any }];

const {
...,
Expand Down
2 changes: 1 addition & 1 deletion packages/hooks/src/useAntdTable/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type Params = [
current: number;
pageSize: number;
sorter?: any;
filter?: any;
filters?: any;
extra?: any;
[key: string]: any;
},
Expand Down
20 changes: 20 additions & 0 deletions packages/hooks/src/useEventListener/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,24 @@ describe('useEventListener', () => {
document.body.click();
expect(state).toBe(1);
});

it('test "enable" parameter', () => {
let state = 0;
let enable = true;
const onClick = () => state++;
const { rerender, unmount } = renderHook(() =>
useEventListener('click', onClick, { target: () => container, enable }),
);

document.body.click();
expect(state).toBe(0);
container.click();
expect(state).toBe(1);

enable = false;
rerender();
container.click();
expect(state).toBe(1);
unmount();
});
});
1 change: 1 addition & 0 deletions packages/hooks/src/useEventListener/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ useEventListener(
| capture | Optional, a Boolean indicating that events of this type will be dispatched to the registered listener before being dispatched to any EventTarget beneath it in the DOM tree. | `boolean` | `false` |
| once | Optional, A Boolean indicating that the listener should be invoked at most once after being added. If true, the listener would be automatically removed when invoked. | `boolean` | `false` |
| passive | Optional, A Boolean which, if true, indicates that the function specified by listener will never call preventDefault(). If a passive listener does call preventDefault(), the user agent will do nothing other than generate a console warning. | `boolean` | `false` |
| enable | Optional, Whether to enable listening. | `boolean` | `true` |
14 changes: 13 additions & 1 deletion packages/hooks/src/useEventListener/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type Options<T extends Target = Target> = {
capture?: boolean;
once?: boolean;
passive?: boolean;
enable?: boolean;
};

function useEventListener<K extends keyof HTMLElementEventMap>(
Expand All @@ -34,13 +35,24 @@ function useEventListener<K extends keyof WindowEventMap>(
handler: (ev: WindowEventMap[K]) => void,
options?: Options<Window>,
): void;
function useEventListener(
eventName: string,
handler: (event: Event) => void,
options?: Options<Window>,
): void;
function useEventListener(eventName: string, handler: noop, options: Options): void;

function useEventListener(eventName: string, handler: noop, options: Options = {}) {
const { enable = true } = options;

const handlerRef = useLatest(handler);

useEffectWithTarget(
() => {
if (!enable) {
return;
}

const targetElement = getTargetElement(options.target, window);
if (!targetElement?.addEventListener) {
return;
Expand All @@ -62,7 +74,7 @@ function useEventListener(eventName: string, handler: noop, options: Options = {
});
};
},
[eventName, options.capture, options.once, options.passive],
[eventName, options.capture, options.once, options.passive, enable],
options.target,
);
}
Expand Down
1 change: 1 addition & 0 deletions packages/hooks/src/useEventListener/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ useEventListener(
| capture | 可选项,listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。 | `boolean` | `false` |
| once | 可选项,listener 在添加之后最多只调用一次。如果是 true,listener 会在其被调用之后自动移除。 | `boolean` | `false` |
| passive | 可选项,设置为 true 时,表示 listener 永远不会调用 preventDefault() 。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。 | `boolean` | `false` |
| enable | 可选项,是否开启监听。 | `boolean` | `true` |
8 changes: 5 additions & 3 deletions packages/hooks/src/useExternal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ const EXTERNAL_USED_COUNT: Record<string, number> = {};

export type Status = 'unset' | 'loading' | 'ready' | 'error';

interface loadResult {
interface LoadResult {
ref: Element;
status: Status;
}

const loadScript = (path: string, props = {}): loadResult => {
type LoadExternal = <T>(path: string, props?: Partial<T>) => LoadResult;

const loadScript: LoadExternal = (path, props = {}) => {
const script = document.querySelector(`script[src="${path}"]`);

if (!script) {
Expand All @@ -58,7 +60,7 @@ const loadScript = (path: string, props = {}): loadResult => {
};
};

const loadCss = (path: string, props = {}): loadResult => {
const loadCss: LoadExternal = (path, props = {}) => {
const css = document.querySelector(`link[href="${path}"]`);
if (!css) {
const newCss = document.createElement('link');
Expand Down
16 changes: 15 additions & 1 deletion packages/hooks/src/useLatest/demo/demo1.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { useLatest } from 'ahooks';

export default () => {
const [count, setCount] = useState(0);
const [count2, setCount2] = useState(0);

const latestCountRef = useLatest(count);

useEffect(() => {
Expand All @@ -21,5 +23,17 @@ export default () => {
return () => clearInterval(interval);
}, []);

return <p>count: {count}</p>;
useEffect(() => {
const interval = setInterval(() => {
setCount2(count2 + 1);
}, 1000);
return () => clearInterval(interval);
}, []);

return (
<>
<p>count(useLatest): {count}</p>
<p>count(defult): {count2}</p>
</>
);
};
25 changes: 23 additions & 2 deletions packages/hooks/src/useLocalStorageState/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { renderHook, act } from '@testing-library/react';
import type { Options } from '../../createUseStorageState';
import useLocalStorageState from '../index';

describe('useLocalStorageState', () => {
const setUp = <T>(key: string, value: T) =>
const setUp = <T>(key: string, value: T, options?: Options<T>) =>
renderHook(() => {
const [state, setState] = useLocalStorageState<T>(key, { defaultValue: value });
const [state, setState] = useLocalStorageState<T>(key, {
defaultValue: value,
...options,
});
return {
state,
setState,
Expand Down Expand Up @@ -106,4 +110,21 @@ describe('useLocalStorageState', () => {
});
expect(hook.result.current.state).toBe('hello world, zhangsan');
});

it('should sync state when changes', async () => {
const LOCAL_STORAGE_KEY = 'test-sync-state';
const hook = setUp(LOCAL_STORAGE_KEY, 'foo', { listenStorageChange: true });
const anotherHook = setUp(LOCAL_STORAGE_KEY, 'bar', { listenStorageChange: true });

expect(hook.result.current.state).toBe('foo');
expect(anotherHook.result.current.state).toBe('bar');

act(() => hook.result.current.setState('baz'));
expect(hook.result.current.state).toBe('baz');
expect(anotherHook.result.current.state).toBe('baz');

act(() => anotherHook.result.current.setState('qux'));
expect(hook.result.current.state).toBe('qux');
expect(anotherHook.result.current.state).toBe('qux');
});
});
38 changes: 38 additions & 0 deletions packages/hooks/src/useLocalStorageState/demo/demo4.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* title: Sync state with localStorage
* desc: When the stored value changes, all `useLocalStorageState` with the same `key` will synchronize their states, including different tabs of the same browser (try to open two tabs of this page, clicking a button on one page will automatically update the "count" on the other page).
*
* title.zh-CN: 将 state 与 localStorage 保持同步
* desc.zh-CN: 存储值变化时,所有 `key` 相同的 `useLocalStorageState` 会同步状态,包括同一浏览器不同 tab 之间(尝试打开两个此页面,点击其中一个页面的按钮,另一个页面的 count 会自动更新)
*/

import React from 'react';
import { useLocalStorageState } from 'ahooks';

export default function () {
return (
<>
<Counter />
<Counter />
</>
);
}

function Counter() {
const [count, setCount] = useLocalStorageState('use-local-storage-state-demo4', {
defaultValue: 0,
listenStorageChange: true,
});

const add = () => setCount(count! + 1);
const clear = () => setCount();

return (
<div style={{ marginBottom: '8px' }}>
<button style={{ marginRight: '8px' }} onClick={add}>
count: {count}
</button>
<button onClick={clear}>Clear</button>
</div>
);
}
17 changes: 11 additions & 6 deletions packages/hooks/src/useLocalStorageState/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ A Hook that store state into localStorage.
<code src="./demo/demo2.tsx"></code>
<code src="./demo/demo3.tsx"></code>

### Sync state with localStorage

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

## API

If you want to delete this record from localStorage, you can use `setState()` or `setState(undefined)`.
Expand Down Expand Up @@ -47,12 +51,13 @@ const [state, setState] = useLocalStorageState<T>(
### Options
| Property | Description | Type | Default |
| ------------ | ----------------------------- | -------------------------- | ----------------------------- |
| defaultValue | Default value | `any \| (() => any)` | - |
| serializer | Custom serialization method | `(value: any) => string` | `JSON.stringify` |
| deserializer | Custom deserialization method | `(value: string) => any` | `JSON.parse` |
| onError | On error callback | `(error: unknown) => void` | `(e) => { console.error(e) }` |
| Property | Description | Type | Default |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | ----------------------------- |
| defaultValue | Default value | `any \| (() => any)` | - |
| listenStorageChange | Whether to listen storage changes. If `true`, when the stored value changes, all `useLocalStorageState` with the same `key` will synchronize their states, including different tabs of the same browser | `boolean` | `false` |
| serializer | Custom serialization method | `(value: any) => string` | `JSON.stringify` |
| deserializer | Custom deserialization method | `(value: string) => any` | `JSON.parse` |
| onError | On error callback | `(error: unknown) => void` | `(e) => { console.error(e) }` |
## Remark
Expand Down
17 changes: 11 additions & 6 deletions packages/hooks/src/useLocalStorageState/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ demo:
<code src="./demo/demo2.tsx"></code>
<code src="./demo/demo3.tsx"></code>

### 将 state 与 localStorage 保持同步

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

## API

如果想从 localStorage 中删除这条数据,可以使用 `setState()``setState(undefined)`
Expand Down Expand Up @@ -47,12 +51,13 @@ const [state, setState] = useLocalStorageState<T>(
### Options
| 参数 | 说明 | 类型 | 默认值 |
| ------------ | ------------------ | -------------------------- | ----------------------------- |
| defaultValue | 默认值 | `any \| (() => any)` | - |
| serializer | 自定义序列化方法 | `(value: any) => string` | `JSON.stringify` |
| deserializer | 自定义反序列化方法 | `(value: string) => any` | `JSON.parse` |
| onError | 错误回调函数 | `(error: unknown) => void` | `(e) => { console.error(e) }` |
| 参数 | 说明 | 类型 | 默认值 |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | ----------------------------- |
| defaultValue | 默认值 | `any \| (() => any)` | - |
| listenStorageChange | 是否监听存储变化。如果是 `true`,当存储值变化时,所有 `key` 相同的 `useLocalStorageState` 会同步状态,包括同一浏览器不同 tab 之间 | `boolean` | `false` |
| serializer | 自定义序列化方法 | `(value: any) => string` | `JSON.stringify` |
| deserializer | 自定义反序列化方法 | `(value: string) => any` | `JSON.parse` |
| onError | 错误回调函数 | `(error: unknown) => void` | `(e) => { console.error(e) }` |
## 备注
Expand Down

0 comments on commit 7340355

Please sign in to comment.