Skip to content
Open
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
15 changes: 15 additions & 0 deletions packages/hooks/src/useEventEmitter/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ describe('useEventEmitter', () => {
event$.useSubscription((val) => {
setCount((c) => c + val + 10);
});
event$.useSubscription(async (val) => {
setCount((c) => c + val + 10);
}, 'async');
return {
event$,
count,
Expand All @@ -30,4 +33,16 @@ describe('useEventEmitter', () => {
});
expect(hook.result.current.count).toBe(26);
});

it('asyncEmit and subscribe should work', async () => {
const hook = setUp();
await act(async () => {
await hook.result.current.event$.asyncEmit(1, 'async');
});
expect(hook.result.current.count).toBe(11);
await act(async () => {
await hook.result.current.event$.asyncEmit(2, 'async');
});
expect(hook.result.current.count).toBe(23);
});
});
55 changes: 55 additions & 0 deletions packages/hooks/src/useEventEmitter/demo/demo2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* title: Invoke asynchronous methods arbitrarily across components
* desc: A component creates a 'getData' event. By calling 'getData' in component B, component A receives the notification
*
* title.zh-CN: 任意跨组件调用异步方法
* desc.zh-CN: A组件创建了一个 `getData` 事件。在 B组件 中调用 `getData` ,A 组件就可以收到通知。
*/

import React, { useRef, FC, useState } from 'react';
import { useEventEmitter } from 'ahooks';

const ComponentA: FC = function (props) {
const inputRef = useRef<any>();
const Bus = useEventEmitter<string>();
const getData = async (type: string) => {
// Your asynchronous operation

return `${type} Get A Data`;
};
Bus.useSubscription(getData, 'A/getData');

return (
<input ref={inputRef} placeholder="Enter reply" style={{ width: '100%', padding: '4px' }} />
);
};

const ComponentB: FC = function (props) {
const Bus = useEventEmitter<string>();
const [data, setData] = useState('none');
return (
<div style={{ paddingBottom: 24 }}>
<p>{data}</p>
<button
type="button"
onClick={async () => {
const results = await Bus.asyncEmit('B', 'A/getData');
if (results) {
setData(results[0]);
}
}}
>
Get
</button>
</div>
);
};

export default function () {
return (
<>
<ComponentA />
<ComponentB />
</>
);
}
36 changes: 35 additions & 1 deletion packages/hooks/src/useEventEmitter/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,45 @@ event$.useSubscription(val => {

If you want to let the child component notify the parent component, you can just use `props` to pass a `onEvent` function. And if you want to let the parent component notify the child component, you can use `forwardRef` to retrieve the ref of child component. `useEventEmitter` is most suitable for event management among multiple components or between two components which are far away.

Call precisely by specifying the event name。

```js
event$.useSubscription(val => {
console.log(val);
},'console');
event$.emit('hello','console')
```

Call the asynchronous method and get the result。

```js
event$.useSubscription(async (val) => {
return `async ${val}`
},'console');
const res = await event$.asyncEmit('hello','console')
console.log(res[0]);
```

Support for incoming dependencies from subscription methods (keep the state value inside the subscription method up to date)

```js
const [status,setStatus] = useState('');
event$.useSubscription((val) => {
console.log("new status:",status)
},'console',[status]);
const res = event$.emit('hello','console')
```

## Examples

### Parent component shares a event

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

### Asynchronous events shared by different components (synchronously executed)

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

## API

### Params
Expand All @@ -49,5 +82,6 @@ const result: Result = useEventEmitter<T>();

| Property | Description | Type |
| --------------- | ----------------------------- | -------------------------------------- |
| emit | Emit a new event. | `(val: T) => void` |
| emit | Emit a new event. | `(val: T) => []:any` |
| asyncEmit | Send an asynchronous event | `(val: T, name?: string) => []:any` |
| useSubscription | Subscribe to a event emitter. | `(callback: (val: T) => void) => void` |
46 changes: 30 additions & 16 deletions packages/hooks/src/useEventEmitter/index.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,46 @@
import { useRef, useEffect } from 'react';

const DEFAULT_SUBSCRIPTION_NAME = 'useEventEmitter-Default-Name';

type Subscription<T> = (val: T) => void;

export class EventEmitter<T> {
private subscriptions = new Set<Subscription<T>>();
// private subscriptions = new Set<Subscription<T>>();
private subscriptions = new Object();

emit = (val: T) => {
for (const subscription of this.subscriptions) {
subscription(val);
emit = (val: T, name?: string) => {
const subscriptions = this.subscriptions[name || DEFAULT_SUBSCRIPTION_NAME];
const result: any[] = [];
if (subscriptions) {
subscriptions.forEach((s: Subscription<T>) => {
result.push(s.call(this, val));
});
}
return result;
};

useSubscription = (callback: Subscription<T>) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const callbackRef = useRef<Subscription<T>>();
callbackRef.current = callback;
asyncEmit = async (val: T, name?: string) => {
const subscriptions = this.subscriptions[name || DEFAULT_SUBSCRIPTION_NAME];
const result: any[] = [];
if (subscriptions) {
for (let i = 0; i < subscriptions.length; i++) {
const s = subscriptions[i];
result.push(await s.call(this, val));
}
}
return result;
};
useSubscription = (callback: Subscription<T>, name?: string, deps?: any[]) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
function subscription(val: T) {
if (callbackRef.current) {
callbackRef.current(val);
}
}
this.subscriptions.add(subscription);
(
this.subscriptions[name || DEFAULT_SUBSCRIPTION_NAME] ||
(this.subscriptions[name || DEFAULT_SUBSCRIPTION_NAME] = [])
).push(callback);
return () => {
this.subscriptions.delete(subscription);
this.subscriptions[name || DEFAULT_SUBSCRIPTION_NAME] = null;
};
}, []);
}, deps || []);
};
}

Expand Down
42 changes: 38 additions & 4 deletions packages/hooks/src/useEventEmitter/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,45 @@ event$.useSubscription(val => {

对于**子组件**通知**父组件**的情况,我们仍然推荐直接使用 `props` 传递一个 `onEvent` 函数。而对于**父组件**通知**子组件**的情况,可以使用 `forwardRef` 获取子组件的 ref ,再进行子组件的方法调用。 `useEventEmitter` 适合的是在**距离较远**的组件之间进行事件通知,或是在**多个**组件之间共享事件通知。

通过指定事件名称精准调用。

```js
event$.useSubscription(val => {
console.log(val);
},'console');
event$.emit('hello','console')
```

调用异步方法并获取结果。

```js
event$.useSubscription(async (val) => {
return `async ${val}`
},'console');
const res = await event$.asyncEmit('hello','console')
console.log(res[0]);
```

支持订阅方法传入依赖项(保证订阅方法内部的state值为最新)

```js
const [status,setStatus] = useState('');
event$.useSubscription((val) => {
console.log("new status:",status)
},'console',[status]);
const res = event$.emit('hello','console')
```

## 代码演示

### 父组件向子组件共享事件

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

### 不同组件共享异步事件(可同步执行)

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

## API

```typescript
Expand All @@ -45,7 +78,8 @@ const result: Result = useEventEmitter<T>();

### Result

| 参数 | 说明 | 类型 |
| --------------- | ---------------- | -------------------------------------- |
| emit | 发送一个事件通知 | `(val: T) => void` |
| useSubscription | 订阅事件 | `(callback: (val: T) => void) => void` |
| 参数 | 说明 | 类型 |
| --------------- | -------------------- | -------------------------------------- |
| emit | 发送一个事件通知 | `(val: T) => []:any` |
| asyncEmit | 发送一个异步事件通知 | `(val: T, name?: string) => []:any` |
| useSubscription | 订阅事件 | `(callback: (val: T) => void) => void` |