@niko464/react-kit 0.0.1 → 0.1.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 +86 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/use-event-listener.d.ts +6 -0
- package/dist/use-event-listener.js +18 -0
- package/dist/use-interval.d.ts +5 -0
- package/dist/use-interval.js +17 -0
- package/dist/use-mount-effect.d.ts +2 -0
- package/dist/use-mount-effect.js +4 -0
- package/package.json +35 -5
package/README.md
CHANGED
|
@@ -1,3 +1,88 @@
|
|
|
1
1
|
# @niko464/react-kit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React hooks that replace `useEffect` in Niko464's TypeScript projects. [`@niko464/eslint-config`](../eslint-config)'s `react` block bans the effect hooks; these three hooks and the patterns in [Instead of an effect](#instead-of-an-effect) cover what effects were used for. None of the hooks takes a dependency list.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @niko464/react-kit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
React 19.2 or later is required: the hooks use `useEffectEvent`.
|
|
12
|
+
|
|
13
|
+
## `useMountEffect(fn)`
|
|
14
|
+
|
|
15
|
+
`fn` runs once after mount. The cleanup it returns runs on unmount. In development, `<StrictMode>` mounts each component twice, so `fn` and its cleanup run one extra time there.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { useMountEffect } from '@niko464/react-kit';
|
|
19
|
+
|
|
20
|
+
const PreviewFrame = ({ draft }: { draft: Draft }) => {
|
|
21
|
+
useMountEffect(() => {
|
|
22
|
+
window.parent.postMessage({ type: 'draft', draft }, '*');
|
|
23
|
+
});
|
|
24
|
+
return <Preview draft={draft} />;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
<PreviewFrame key={message.id} draft={message.draft} />;
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
To run it again when a value changes, give the component a `key` with that value: React mounts a new one.
|
|
31
|
+
|
|
32
|
+
## `useEventListener(target, type, handler, options?)`
|
|
33
|
+
|
|
34
|
+
Listens to `type` on `target` while the component is mounted.
|
|
35
|
+
|
|
36
|
+
- `target` is `window`, `document` or a ref to an element. A ref must point to its element when the component mounts.
|
|
37
|
+
- The event type is checked against the target, so `'keydown'` on `window` gives a `KeyboardEvent`.
|
|
38
|
+
- `handler` always sees the latest props and state, without re-subscribing.
|
|
39
|
+
- `options` are `addEventListener`'s options. Changing `target`, `type` or `options` re-subscribes.
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { useEventListener } from '@niko464/react-kit';
|
|
43
|
+
import { useRef } from 'react';
|
|
44
|
+
|
|
45
|
+
const Dialog = ({ onClose }: { onClose: () => void }) => {
|
|
46
|
+
useEventListener(window, 'keydown', (event) => {
|
|
47
|
+
if (event.key === 'Escape') onClose();
|
|
48
|
+
});
|
|
49
|
+
return <div role="dialog">…</div>;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const ColourPicker = ({ onPick }: { onPick: (colour: string) => void }) => {
|
|
53
|
+
const input = useRef<HTMLInputElement>(null);
|
|
54
|
+
useEventListener(input, 'change', () => {
|
|
55
|
+
if (input.current) onPick(input.current.value);
|
|
56
|
+
});
|
|
57
|
+
return <input ref={input} type="color" />;
|
|
58
|
+
};
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## `useInterval(fn, ms, { delay, enabled })`
|
|
62
|
+
|
|
63
|
+
`fn` runs every `ms` milliseconds while `enabled`.
|
|
64
|
+
|
|
65
|
+
- `delay` is the wait before the first run, in milliseconds. It defaults to `ms`.
|
|
66
|
+
- `enabled` defaults to `true`. When it turns `false`, the interval stops.
|
|
67
|
+
- Changing `ms`, `delay` or `enabled` restarts the interval from the beginning.
|
|
68
|
+
- `fn` always sees the latest props and state. Changing it does not restart the interval.
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { useInterval } from '@niko464/react-kit';
|
|
72
|
+
import { useState } from 'react';
|
|
73
|
+
|
|
74
|
+
const Slideshow = ({ photos, paused }: { photos: Photo[]; paused: boolean }) => {
|
|
75
|
+
const [index, setIndex] = useState(0);
|
|
76
|
+
useInterval(() => setIndex((index + 1) % photos.length), 4000, { enabled: !paused });
|
|
77
|
+
return <img src={photos[index]?.url} alt="" />;
|
|
78
|
+
};
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Instead of an effect
|
|
82
|
+
|
|
83
|
+
| To | Use |
|
|
84
|
+
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
85
|
+
| Start over when a value changes | A `key` with that value, so React mounts a new component |
|
|
86
|
+
| Measure, place or set up a DOM node | A ref callback: `<img ref={(node) => place(node)} />`, which may return a cleanup |
|
|
87
|
+
| Keep a value in sync with props or state | Derive it during render: `const total = items.reduce(…)`, not a state that an effect updates |
|
|
88
|
+
| Animate | [Motion](https://motion.dev): `<motion.div animate={{ x }} />`, its `useSpring`, and `drag` for swipes |
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
type ListenerOptions = boolean | AddEventListenerOptions;
|
|
3
|
+
export declare function useEventListener<Type extends keyof WindowEventMap>(target: Window, type: Type, handler: (event: WindowEventMap[Type]) => void, options?: ListenerOptions): void;
|
|
4
|
+
export declare function useEventListener<Type extends keyof DocumentEventMap>(target: Document, type: Type, handler: (event: DocumentEventMap[Type]) => void, options?: ListenerOptions): void;
|
|
5
|
+
export declare function useEventListener<Type extends keyof HTMLElementEventMap>(target: RefObject<HTMLElement | null>, type: Type, handler: (event: HTMLElementEventMap[Type]) => void, options?: ListenerOptions): void;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useEffect, useEffectEvent } from 'react';
|
|
2
|
+
export function useEventListener(target, type, handler, options) {
|
|
3
|
+
const onEvent = useEffectEvent(handler);
|
|
4
|
+
const { capture = false, once = false, passive, signal, } = typeof options === 'boolean' ? { capture: options } : (options ?? {});
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
const element = 'addEventListener' in target ? target : target.current;
|
|
7
|
+
if (element === null)
|
|
8
|
+
return;
|
|
9
|
+
const listener = (event) => onEvent(event);
|
|
10
|
+
const listenerOptions = { capture, once };
|
|
11
|
+
if (passive !== undefined)
|
|
12
|
+
listenerOptions.passive = passive;
|
|
13
|
+
if (signal !== undefined)
|
|
14
|
+
listenerOptions.signal = signal;
|
|
15
|
+
element.addEventListener(type, listener, listenerOptions);
|
|
16
|
+
return () => element.removeEventListener(type, listener, { capture });
|
|
17
|
+
}, [target, type, capture, once, passive, signal]);
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useEffect, useEffectEvent } from 'react';
|
|
2
|
+
export const useInterval = (fn, ms, { delay = ms, enabled = true } = {}) => {
|
|
3
|
+
const tick = useEffectEvent(fn);
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
if (!enabled)
|
|
6
|
+
return;
|
|
7
|
+
let intervalId;
|
|
8
|
+
const timeoutId = setTimeout(() => {
|
|
9
|
+
tick();
|
|
10
|
+
intervalId = setInterval(() => tick(), ms);
|
|
11
|
+
}, delay);
|
|
12
|
+
return () => {
|
|
13
|
+
clearTimeout(timeoutId);
|
|
14
|
+
clearInterval(intervalId);
|
|
15
|
+
};
|
|
16
|
+
}, [ms, delay, enabled]);
|
|
17
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@niko464/react-kit",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
5
|
-
"repository": {
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React hooks that replace useEffect: useMountEffect, useEventListener and useInterval",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/Niko464/conventions.git",
|
|
8
|
+
"directory": "packages/react-kit"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"react": "^19.2.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@testing-library/dom": "10.4.2",
|
|
26
|
+
"@testing-library/react": "16.3.3",
|
|
27
|
+
"@types/react": "19.3.0",
|
|
28
|
+
"@types/react-dom": "19.3.0",
|
|
29
|
+
"jsdom": "30.1.2",
|
|
30
|
+
"react": "19.3.0",
|
|
31
|
+
"react-dom": "19.3.0"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
35
|
+
"test": "vitest run"
|
|
36
|
+
}
|
|
37
|
+
}
|