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: 增加usePublisherState Hook #2329

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
1 change: 1 addition & 0 deletions config/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const menus = [
'useSafeState',
'useGetState',
'useResetState',
'usePublisherState',
],
},
{
Expand Down
4 changes: 4 additions & 0 deletions packages/hooks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import useVirtualList from './useVirtualList';
import useWebSocket from './useWebSocket';
import useWhyDidYouUpdate from './useWhyDidYouUpdate';
import useMutationObserver from './useMutationObserver';
import { usePublisherState, useSubscriberState, PublisherStateType } from './usePublisherState';

export {
useRequest,
Expand Down Expand Up @@ -156,4 +157,7 @@ export {
useRafTimeout,
useResetState,
useMutationObserver,
usePublisherState,
useSubscriberState,
PublisherStateType,
};
18 changes: 18 additions & 0 deletions packages/hooks/src/usePublisherState/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { renderHook, act } from '@testing-library/react';
import { usePublisherState, useSubscriberState } from '../index';

describe('usePublisherState', () => {
it('should work', () => {
const { result } = renderHook(() => usePublisherState(0));
const { result: subscriberResult } = renderHook(() => useSubscriberState(result.current[0]));
const setRafState = result.current[1];
expect(result.current[0].current).toBe(0);
expect(subscriberResult.current).toBe(0);

act(() => {
setRafState(1);
});
expect(result.current[0].current).toBe(1);
expect(subscriberResult.current).toBe(1);
});
});
47 changes: 47 additions & 0 deletions packages/hooks/src/usePublisherState/demo/demo1.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* title: Default usage
*
* title.zh-CN: 基础用法
*/

import { usePublisherState, useSubscriberState } from 'ahooks';
import type { PublisherStateType } from 'ahooks';
import React, { useState } from 'react';
const InnerComp: React.FC<{ data: PublisherStateType<number>; data1: number }> = ({
data,
data1,
}) => {
const counter = useSubscriberState(data);
return (
<div>
<p>
Child counter: {counter},{data1}
</p>
</div>
);
};
export default () => {
const [counter, setCounter] = usePublisherState(0);
const [counter1, setCounter1] = useState(0);
return (
<div>
<p>
<button
type="button"
onClick={() => setCounter((val) => val + 1)}
style={{ margin: '0 16px' }}
>
counter++ with usePublisherState
</button>
<button type="button" onClick={() => setCounter1((val) => val + 1)}>
counter++ with useState
</button>
</p>

<p>
Parent counter: {counter.current},{counter1}
</p>
<InnerComp data={counter} data1={counter1} />
</div>
);
};
48 changes: 48 additions & 0 deletions packages/hooks/src/usePublisherState/index.en-US.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
nav:
path: /hooks
---

# usePublisherState

Hooks for managing state using a publish-subscribe method, combined with `useSubscriberState`. Updating the state will only trigger re-rendering of components that actively subscribe to it through `useSubscriberState`.

## Example

### Default usage

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

## API

### usePublisherState

```typescript
type PublisherStateType<T> = {
current: T;
observable: Observable<string>;
};
type SetPublisherStateType<T> = (newVal: T | ((newVal: T) => T), needUpdate?: boolean) => void;

const [state, setState] = usePublisherState<T>(
initialState: T,
): [PublisherStateType, SetPublisherStateType]
```

Hooks for defining and managing state using the publish-subscribe method. It returns a tuple containing the current state object and a function to update the state.

- `initialState: T`: The initial value of the state.

### useSubscriberState

```typescript
const state = useSubscriberState<T>(
state: PublisherStateType<T>,
): T
```

Hooks for subscribing to state updates using the publish-subscribe method. It returns the current value of the state.

- `state: PublisherStateType<T>`: The state object returned by `usePublisherState`.

Translate this documentation into English.
65 changes: 65 additions & 0 deletions packages/hooks/src/usePublisherState/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useEffect, useRef, useState } from 'react';

type Observer<T> = (value: T) => void;

class Observable<T> {
private observers: Observer<T>[] = [];

subscribe(observer: Observer<T>): void {
this.observers.push(observer);
}

unsubscribe(observer: Observer<T>): void {
this.observers = this.observers.filter((subscriber) => subscriber !== observer);
}

notify(data: T): void {
this.observers.forEach((observer) => observer(data));
}
}

export type PublisherStateType<T> = {
current: T;
observable: Observable<string>;
};
type SetPublisherStateType<T> = (newVal: T | ((newVal: T) => T), needUpdate?: boolean) => void;

export function usePublisherState<T>(initialState: T) {
const stateRef = useRef<PublisherStateType<T>>({
current: initialState,
observable: new Observable<string>(),
});

const setState: SetPublisherStateType<T> = (
newVal: T | ((oldVal: T) => T),
needUpdate = true,
) => {
if (typeof newVal === 'function') {
stateRef.current.current = (newVal as (newVal: T) => T)(stateRef.current.current);
if (needUpdate) stateRef.current.observable.notify('update');
} else if (stateRef.current.current !== newVal) {
stateRef.current.current = newVal;
if (needUpdate) stateRef.current.observable.notify('update');
}
};

return [stateRef.current, setState] as const;
}

export function useSubscriberState<T>(state?: PublisherStateType<T>) {
const [, forceUpdate] = useState({});
const update = () => forceUpdate({});
useEffect(() => {
if (!state) return;
const observer: Observer<string> = (info) => {
if (info === 'update') update();
};
state.observable.subscribe(observer);

return () => {
state.observable.unsubscribe(observer);
};
}, [state]);

return state?.current;
}
46 changes: 46 additions & 0 deletions packages/hooks/src/usePublisherState/index.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
nav:
path: /hooks
---

# usePublisherState

基于发布订阅的 state 方法的 Hooks,与`useSubscriberState`结合使用。更新 state 时只会触发主动通过`useSubscriberState`接收的组件重渲染。

## 代码演示

### 基础用法

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

## API

### usePublisherState

```typescript
type PublisherStateType<T> = {
current: T;
observable: Observable<string>;
};
type SetPublisherStateType<T> = (newVal: T | ((newVal: T) => T), needUpdate?: boolean) => void;

const [state, setState] = usePublisherState<T>(
initialState: T,
): [PublisherStateType, SetPublisherStateType]
```

基于发布订阅的 state 方法的 Hooks。用于定义和管理状态。返回一个元组,包含当前状态对象和更新状态的方法。

- `initialState: T`: 初始状态的值。

### useSubscriberState

```typescript
const state = useSubscriberState<T>(
state: PublisherStateType<T>,
): T
```

基于发布订阅的 state 方法的 Hooks。用于订阅状态的更新。返回当前状态的值。

- `state: PublisherStateType<T>`: `usePublisherState`返回的状态对象。
Loading