@alcadur/react-events-hook 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # react-events-hook
2
+
3
+ Simple Observer pattern for React.
4
+
5
+ ## What for?
6
+ Useful for communication between components in different parts of the view.
7
+
8
+ Using standard React mechanisms, it's also possible to synchronize with external events.
9
+
10
+ ## Installation
11
+ [soon]
12
+
13
+ ## Usage
14
+ [soon]
package/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './src';
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@alcadur/react-events-hook",
3
+ "version": "1.0.0",
4
+ "description": "no",
5
+ "keywords": [
6
+ "react",
7
+ "react",
8
+ "hook",
9
+ "react",
10
+ "hooks",
11
+ "events",
12
+ "events",
13
+ "hook"
14
+ ],
15
+ "homepage": "https://github.com/Alcadur/react-events-hook#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/Alcadur/react-events-hook/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/Alcadur/react-events-hook.git"
22
+ },
23
+ "license": "MIT",
24
+ "author": "Alcadur",
25
+ "type": "module",
26
+ "main": "index.ts",
27
+ "scripts": {
28
+ "test": "vitest run"
29
+ },
30
+ "peerDependencies": {
31
+ "react": "*18.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@rollup/plugin-typescript": "^12.3.0",
35
+ "@testing-library/dom": "^10.4.1",
36
+ "@testing-library/jest-dom": "^6.9.1",
37
+ "@testing-library/react": "^14.3.1",
38
+ "@types/react": "^18.3.28",
39
+ "@types/react-dom": "^18.3.7",
40
+ "@vitejs/plugin-react": "^6.0.1",
41
+ "jsdom": "^29.0.2",
42
+ "react": "^18.3.1",
43
+ "react-dom": "^18.3.1",
44
+ "rollup": "^4.59.0",
45
+ "typescript": "^5.9.3",
46
+ "vitest": "^4.1.3"
47
+ }
48
+ }
@@ -0,0 +1,11 @@
1
+ import { sharedEvents } from "../shared-events";
2
+ import { EventNameType } from "../events.model";
3
+
4
+ export function emitEvent(event: EventNameType, ...args: any[]) {
5
+ const callbacks = sharedEvents[event];
6
+ if (!callbacks) {
7
+ return;
8
+ }
9
+
10
+ callbacks.forEach(callback => callback(...args));
11
+ }
@@ -0,0 +1,77 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { sharedEvents } from "../shared-events";
3
+ import { emitEvent } from "./emit-event";
4
+ import { onEvent } from "./on-event";
5
+ import { removeEvent } from "./remove-event";
6
+
7
+ describe("actions", () => {
8
+ beforeEach(() => {
9
+ Object.keys(sharedEvents).forEach((key) => delete sharedEvents[key]);
10
+ });
11
+
12
+ describe("onEvent", () => {
13
+ it("registers a single callback", () => {
14
+ const callback = vi.fn();
15
+
16
+ onEvent("test-event", callback);
17
+
18
+ expect(sharedEvents["test-event"]).toEqual([callback]);
19
+ });
20
+
21
+ it("registers an array of callbacks", () => {
22
+ const callback1 = vi.fn();
23
+ const callback2 = vi.fn();
24
+
25
+ onEvent("test-event", [callback1, callback2]);
26
+
27
+ expect(sharedEvents["test-event"]).toEqual([callback1, callback2]);
28
+ });
29
+ });
30
+
31
+ describe("emitEvent", () => {
32
+ it("invokes all callbacks with provided args", () => {
33
+ const callback1 = vi.fn();
34
+ const callback2 = vi.fn();
35
+
36
+ onEvent("test-event", [callback1, callback2]);
37
+ emitEvent("test-event", "data", 1);
38
+
39
+ expect(callback1).toHaveBeenCalledWith("data", 1);
40
+ expect(callback2).toHaveBeenCalledWith("data", 1);
41
+ });
42
+
43
+ it("does nothing when event has no callbacks", () => {
44
+ expect(() => emitEvent("missing-event", "data")).not.toThrow();
45
+ });
46
+ });
47
+
48
+ describe("removeEvent", () => {
49
+ it("removes a single callback", () => {
50
+ const callback = vi.fn();
51
+
52
+ onEvent("test-event", callback);
53
+ removeEvent("test-event", callback);
54
+ emitEvent("test-event", "data");
55
+
56
+ expect(callback).not.toHaveBeenCalled();
57
+ });
58
+
59
+ it("removes multiple callbacks from array input", () => {
60
+ const callback1 = vi.fn();
61
+ const callback2 = vi.fn();
62
+
63
+ onEvent("test-event", [callback1, callback2]);
64
+ removeEvent("test-event", [callback1, callback2]);
65
+ emitEvent("test-event", "data");
66
+
67
+ expect(callback1).not.toHaveBeenCalled();
68
+ expect(callback2).not.toHaveBeenCalled();
69
+ });
70
+
71
+ it("does nothing when event has no callbacks", () => {
72
+ const callback = vi.fn();
73
+
74
+ expect(() => removeEvent("missing-event", callback)).not.toThrow();
75
+ });
76
+ });
77
+ });
@@ -0,0 +1,3 @@
1
+ export * from './emit-event';
2
+ export * from './on-event';
3
+ export * from './remove-event';
@@ -0,0 +1,11 @@
1
+ import { sharedEvents } from "../shared-events";
2
+ import { getAsArray } from "../utils";
3
+ import { EventCallbackType, EventNameType } from "../events.model";
4
+
5
+ export function onEvent(event: EventNameType, callback: EventCallbackType[] | EventCallbackType) {
6
+ const callbacks = sharedEvents[event] ?? [];
7
+
8
+ callbacks.push(...getAsArray(callback));
9
+
10
+ sharedEvents[event] = callbacks;
11
+ }
@@ -0,0 +1,17 @@
1
+ import { sharedEvents } from "../shared-events";
2
+ import { getAsArray } from "../utils";
3
+ import { EventCallbackType, EventNameType } from "../events.model";
4
+
5
+ export function removeEvent(event: EventNameType, callback: EventCallbackType | EventCallbackType[]) {
6
+ const declaredCallbacks = sharedEvents[event];
7
+ if (!declaredCallbacks) {
8
+ return;
9
+ }
10
+
11
+ getAsArray(callback).forEach(cb => {
12
+ const index = declaredCallbacks.indexOf(cb);
13
+ if (index !== -1) {
14
+ declaredCallbacks.splice(index, 1);
15
+ }
16
+ })
17
+ }
@@ -0,0 +1,4 @@
1
+ export type EventNameType = string | number | symbol
2
+ export type EventCallbackType = ((...args: any[]) => void) | (() => void)
3
+
4
+ export type InitEventsType = { [key: EventNameType]: EventCallbackType[] | EventCallbackType }
@@ -0,0 +1 @@
1
+ export * from './useEvents';
@@ -0,0 +1,53 @@
1
+ import { renderHook, act } from '@testing-library/react';
2
+ import { useEvents } from './useEvents';
3
+ import { describe, it, expect, beforeEach, vi } from "vitest";
4
+ import { sharedEvents } from '../shared-events';
5
+
6
+ describe('useEvents', () => {
7
+ beforeEach(() => {
8
+ Object.keys(sharedEvents).forEach(key => delete sharedEvents[key]);
9
+ });
10
+
11
+ it('should register and emit events', () => {
12
+ const { result } = renderHook(() => useEvents());
13
+ const callback = vi.fn();
14
+
15
+ act(() => {
16
+ result.current.onEvent('test-event', callback);
17
+ });
18
+
19
+ act(() => {
20
+ result.current.emitEvent('test-event', 'data');
21
+ });
22
+
23
+ expect(callback).toHaveBeenCalledWith('data');
24
+ });
25
+
26
+ it('should remove events', () => {
27
+ const { result } = renderHook(() => useEvents());
28
+ const callback = vi.fn();
29
+
30
+ act(() => {
31
+ result.current.onEvent('test-event', callback);
32
+ result.current.removeEvent('test-event', callback);
33
+ });
34
+
35
+ act(() => {
36
+ result.current.emitEvent('test-event', 'data');
37
+ });
38
+
39
+ expect(callback).not.toHaveBeenCalled();
40
+ });
41
+
42
+ it('should register events from init map', () => {
43
+ const callback = vi.fn();
44
+ renderHook(() => useEvents({ 'init-event': callback }));
45
+
46
+ const { result } = renderHook(() => useEvents());
47
+ act(() => {
48
+ result.current.emitEvent('init-event', 'init-data');
49
+ });
50
+
51
+ expect(callback).toHaveBeenCalledWith('init-data');
52
+ });
53
+ });
@@ -0,0 +1,23 @@
1
+ import { useEffect } from "react";
2
+ import { InitEventsType } from "../events.model";
3
+ import { emitEvent, onEvent, removeEvent } from "../actions";
4
+
5
+ export const useEvents = (eventMap: InitEventsType = {}) => {
6
+ useEffect(() => {
7
+ if (!eventMap) {
8
+ return;
9
+ }
10
+ const eventMapEntries = Object.entries(eventMap);
11
+ eventMapEntries.forEach(([event, callback]) => {
12
+ onEvent(event, callback)
13
+ })
14
+
15
+ return () => {
16
+ eventMapEntries.forEach(([event, callback]) => {
17
+ removeEvent(event, callback)
18
+ })
19
+ }
20
+ }, [eventMap])
21
+
22
+ return { onEvent, emitEvent, removeEvent };
23
+ };
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './hooks';
2
+ export * from './utils';
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';
@@ -0,0 +1,3 @@
1
+ import type { EventCallbackType, EventNameType } from "./events.model";
2
+
3
+ export const sharedEvents: { [key: EventNameType]: EventCallbackType[] } = {};
@@ -0,0 +1,16 @@
1
+ import { getAsArray } from './utils';
2
+ import { describe, it, expect, vi } from "vitest";
3
+
4
+ describe('utils', () => {
5
+ describe('getAsArray', () => {
6
+ it('should return the same array if an array is passed', () => {
7
+ const arr = [vi.fn()];
8
+ expect(getAsArray(arr)).toBe(arr);
9
+ });
10
+
11
+ it('should wrap a single value in an array', () => {
12
+ const callback = vi.fn();
13
+ expect(getAsArray(callback)).toEqual([callback]);
14
+ });
15
+ });
16
+ });
package/src/utils.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { EventCallbackType } from "./events.model";
2
+
3
+ export function getAsArray(value: EventCallbackType[] | EventCallbackType): EventCallbackType[] {
4
+ return Array.isArray(value) ? value : [value];
5
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "outDir": "dist",
4
+ "jsx": "react",
5
+ "skipLibCheck": true,
6
+ "allowSyntheticDefaultImports": true,
7
+ "declaration": true,
8
+ "lib": ["esnext"],
9
+ "strictNullChecks": true,
10
+ },
11
+ "include": ["src"],
12
+ "exclude": ["example"]
13
+ }
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ test: {
7
+ environment: 'jsdom',
8
+ globals: true,
9
+ type: 'module',
10
+ setupFiles: './src/setupTests.ts',
11
+ exclude: ['dist/**', 'node_modules/**']
12
+ },
13
+ });