nanoeffects 0.16.0 → 0.18.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 +2 -0
- package/dist/react.d.ts +23 -0
- package/dist/react.js +1 -1
- package/docs/react.md +62 -0
- package/docs/starter.md +77 -0
- package/llms-full.txt +144 -1
- package/llms.txt +3 -2
- package/package.json +8 -6
package/README.md
CHANGED
|
@@ -15,6 +15,8 @@ npm install nanoeffects
|
|
|
15
15
|
|
|
16
16
|
ESM only, no dependencies, types included.
|
|
17
17
|
|
|
18
|
+
Starting a new project? `npm create nanoeffects@latest my-app` makes a Vite app with a bus ([Docs](docs/starter.md)).
|
|
19
|
+
|
|
18
20
|
Want to try it first? [Open the playground](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/playground): a search box with `debounce` and `latest`, running in your browser, no install. More playgrounds (React) and recipes: [Examples](examples/README.md).
|
|
19
21
|
|
|
20
22
|
## Create a bus
|
package/dist/react.d.ts
CHANGED
|
@@ -24,7 +24,30 @@ type ReactApi<E extends EventMap> = {
|
|
|
24
24
|
<K extends EventKey<E>>(creator: FreeActionCreator<K>, value: E[K], options?: UseEmitterOptions<E[K]>): void;
|
|
25
25
|
<P, T extends string>(creator: ActionCreator<P, T>, value: P, options?: UseEmitterOptions<P>): void;
|
|
26
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* A React hook with the signature of `useState`: returns `[state, setState]`, and
|
|
29
|
+
* every emit of `event` sets the state to its payload while the component is
|
|
30
|
+
* mounted. `initial` may be a function, as in `useState`. The bus keeps no values,
|
|
31
|
+
* so an event emitted before the first commit is not seen: read a current value
|
|
32
|
+
* into `initial` when that matters. The method does not read `this`, so
|
|
33
|
+
* `export const { useEventState } = bus` gives a free hook.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```tsx
|
|
37
|
+
* const [found, setFound] = useEventState(searchLoaded, []); // [] until the first searchLoaded
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
useEventState: {
|
|
41
|
+
<K extends EventKey<E>, S>(event: K, initial: StateOf<E[K], S> | (() => StateOf<E[K], S>)): EventState<StateOf<E[K], S>>;
|
|
42
|
+
/** A creator without a payload type: the payload comes from the event map. */
|
|
43
|
+
<K extends EventKey<E>, S>(creator: FreeActionCreator<K>, initial: StateOf<E[K], S> | (() => StateOf<E[K], S>)): EventState<StateOf<E[K], S>>;
|
|
44
|
+
<P, T extends string>(creator: ActionCreator<P, T>, initial: P | (() => P)): EventState<P>;
|
|
45
|
+
};
|
|
27
46
|
};
|
|
47
|
+
/** What `useEventState` returns: the pair of `useState`, with the payload of the event as state. */
|
|
48
|
+
type EventState<P> = [P, (next: P | ((previous: P) => P)) => void];
|
|
49
|
+
type Widen<S> = [S] extends [never[]] ? unknown[] : S;
|
|
50
|
+
type StateOf<P, S> = unknown extends P ? Widen<S> : P;
|
|
28
51
|
/**
|
|
29
52
|
* Adds `bus.useEmitter(event, value)`: a hook that tells the bus when a value of
|
|
30
53
|
* a component changes, without the traps of `useEffect(() => emit(value), [value])`
|
package/dist/react.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as e}from"./with-policy-CW50Edgk.js";import{t}from"./origin-key-BbWN3gmN.js";import{useEffect as n,useMemo as r,useRef as i}from"react";const
|
|
1
|
+
import{t as e}from"./with-policy-CW50Edgk.js";import{t}from"./origin-key-BbWN3gmN.js";import{useEffect as n,useMemo as r,useRef as i,useState as a}from"react";const o=()=>e=>{function r(r,a,o){let s=i(a),c=i({event:r,options:o});n(()=>{c.current={event:r,options:o}}),n(()=>{let{event:n,options:r}=c.current,i=r?.equals??Object.is,o=s.current;if(i(o,a))return;s.current=a;let l=typeof n==`function`?n(a):{type:n,payload:a},u=Reflect.get(e,t);u?u(l,r??{}):e.emit(l)},[a])}function o(t,r){let[i,o]=a(r);return n(()=>{let n=t;return e.on(n,e=>{let t=e;o(()=>t)})},[t]),[i,o]}return Object.assign(e,{useEmitter:r,useEventState:o})},s=(t,a,o=[])=>{let s=i(t);n(()=>{s.current=t});let c=r(()=>e(a((e,t)=>s.current(e,t))),o);return n(()=>()=>c.cancel(),[c]),c};export{o as reactPlugin,s as usePolicy};
|
package/docs/react.md
CHANGED
|
@@ -162,6 +162,68 @@ const checkName = latest(async (name, ctx) => {
|
|
|
162
162
|
|
|
163
163
|
In the component, add `bus.emit("form/closed")` to the cleanup function of the effect. This is optional: skip it when finishing a request after unmount does no harm, as for a cheap check.
|
|
164
164
|
|
|
165
|
+
## Show the last value of an event as state
|
|
166
|
+
|
|
167
|
+
**Problem.** A component often needs the latest result of an event, such as the found items of a search. The hand-written form is easy to get slightly wrong:
|
|
168
|
+
|
|
169
|
+
<!-- docs-check: skip - the hand-written form that this section replaces -->
|
|
170
|
+
```jsx
|
|
171
|
+
useEffect(() => bus.on(searchLoaded, setFound), []);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
It needs a `useState` next to it, the empty `[]` hides the dependency on the event, and it breaks if the payload is a function, because React calls `setFound(payload)` as an updater.
|
|
175
|
+
|
|
176
|
+
**When.** The value you show comes from the bus, not from the component. For the opposite direction, a value of the component that the bus must hear, use `useEmitter`.
|
|
177
|
+
|
|
178
|
+
`useEventState(event, initial)` has the signature of `useState`: it returns `[state, setState]`. `initial` is the state before the first event and may be a function, as in `useState`. After that, every emit of `event` sets the state to its payload. The hook unsubscribes when the component unmounts.
|
|
179
|
+
|
|
180
|
+
```js
|
|
181
|
+
// bus.js
|
|
182
|
+
import { createBus } from "nanoeffects";
|
|
183
|
+
import { reactPlugin } from "nanoeffects/react";
|
|
184
|
+
|
|
185
|
+
// createBus — the event bus, created once outside React
|
|
186
|
+
// reactPlugin — adds the hooks bus.useEmitter and bus.useEventState
|
|
187
|
+
export const bus = createBus().use(reactPlugin());
|
|
188
|
+
export const { useEventState } = bus; // a free hook: components import it without the bus
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
// search.js
|
|
193
|
+
import { createAction } from "nanoeffects/actions";
|
|
194
|
+
|
|
195
|
+
// createAction — the event name; the handlers emit it with the found titles
|
|
196
|
+
export const searchLoaded = createAction("search/loaded");
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```jsx
|
|
200
|
+
// Found.jsx
|
|
201
|
+
import { useEventState } from "./bus";
|
|
202
|
+
import { searchLoaded } from "./search";
|
|
203
|
+
|
|
204
|
+
export function Found() {
|
|
205
|
+
// useEventState — like useState([]), but searchLoaded sets the value
|
|
206
|
+
const [found, setFound] = useEventState(searchLoaded, []);
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<>
|
|
210
|
+
<ul>
|
|
211
|
+
{found.map((title) => (
|
|
212
|
+
<li key={title}>{title}</li>
|
|
213
|
+
))}
|
|
214
|
+
</ul>
|
|
215
|
+
<button onClick={() => setFound([])}>Clear</button>
|
|
216
|
+
</>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
`setFound` is the setter of React. It changes the state of this component only and emits nothing.
|
|
222
|
+
|
|
223
|
+
In TypeScript, the state type comes from the event: from `createAction<string[]>("search/loaded")` or from the event map of the bus. When the event has no payload type, as in the example above, it comes from `initial`: `["a"]` is `string[]`, and an empty `[]` is `unknown[]`. Give the event a type, or pass a typed `initial`, when you need more.
|
|
224
|
+
|
|
225
|
+
The bus keeps no values, so the hook cannot replay an event. An event emitted before the component mounts is not seen: if that matters, read the current value into `initial`.
|
|
226
|
+
|
|
165
227
|
## Keep a debounced handler between renders
|
|
166
228
|
|
|
167
229
|
**Problem.** A component runs again on every render. A policy made in its body, such as `debounce(300, search)`, is made again each time and loses its timer. A policy made once keeps the callback of the first render, which reads old props and state.
|
package/docs/starter.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Start a project from a template
|
|
2
|
+
|
|
3
|
+
Use this page when you want a running project in one command instead of wiring Vite and the bus by hand. It makes a small app with a search box that waits for you to stop typing (`debounce`) and aborts the older request when a newer one starts (`latest`).
|
|
4
|
+
|
|
5
|
+
You need Node 18 or newer.
|
|
6
|
+
|
|
7
|
+
## Create the project
|
|
8
|
+
|
|
9
|
+
Run the command for your package manager. Replace `my-app` with the name of the new directory.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm create nanoeffects@latest my-app
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm create nanoeffects my-app
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
yarn create nanoeffects my-app
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bun create nanoeffects my-app
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The command asks for the template when you do not give one. The directory must be new or empty: the command stops instead of overwriting files.
|
|
28
|
+
|
|
29
|
+
Then follow the steps it prints:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cd my-app
|
|
33
|
+
npm install
|
|
34
|
+
npm run dev
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Open the address that Vite prints and type in the box. The fake server answers after 800 ms, so you can see the older request being aborted.
|
|
38
|
+
|
|
39
|
+
## Choose a template
|
|
40
|
+
|
|
41
|
+
Pass `--template` to skip the question. With npm, put `--` before the option.
|
|
42
|
+
|
|
43
|
+
| Template | What it shows | Use it when |
|
|
44
|
+
| -------- | ------------- | ----------- |
|
|
45
|
+
| `vanilla` | A bus, one event and a `debounce` plus `latest` handler, with plain DOM code | You use no framework, or want to see the bus alone |
|
|
46
|
+
| `react` | The same handler with `useEmitter` and `useEventState`, and `usePolicy` for a debounced save in a component | You build a React app |
|
|
47
|
+
| `vanilla-ts` | The `vanilla` app in TypeScript | You want the compiler to check events and handlers |
|
|
48
|
+
| `react-ts` | The `react` app in TypeScript | You build a React app in TypeScript |
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm create nanoeffects@latest my-app -- --template react
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The TypeScript templates are the same apps with the same files, written in `.ts` and `.tsx`. Events are made with `createAction<Payload>`, so handlers and `useEventState` get their types from the events, with no annotation in the component. `npm run build` type-checks the project (`tsc --noEmit`) before bundling it.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm create nanoeffects@latest my-app -- --template react-ts
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Read the files
|
|
61
|
+
|
|
62
|
+
Every template splits the code the way [the guide](guide.md) does: one job per file. In the TypeScript templates the files end in `.ts` (and `.tsx` for components).
|
|
63
|
+
|
|
64
|
+
| File | Job |
|
|
65
|
+
| ---- | --- |
|
|
66
|
+
| `src/bus.js` | Creates the bus. It imports no events. |
|
|
67
|
+
| `src/search.js` | Names the events with `createAction`. It imports no bus. |
|
|
68
|
+
| `src/search-handlers.js` | Handles the events with `debounce` and `latest`. It knows the bus and the events. |
|
|
69
|
+
| `src/api.js` | A fake server. Replace it with your own requests. |
|
|
70
|
+
|
|
71
|
+
Every call of nanoeffects in these files has a comment that says what it does. To make it your own, replace `src/api.js`, then rename the events and the handler.
|
|
72
|
+
|
|
73
|
+
## Next steps
|
|
74
|
+
|
|
75
|
+
- [Policies](policies.md): `leading`, `throttle` and `queue` next to `latest`.
|
|
76
|
+
- [Build a sign-up form in React](react.md): the same ideas in a bigger React example.
|
|
77
|
+
- [Package entries](package.md): which import holds what.
|
package/llms-full.txt
CHANGED
|
@@ -9,7 +9,7 @@ Rules that generated code often gets wrong:
|
|
|
9
9
|
- A handler gets the payload and a task context: `(payload, ctx)`. The context has `signal`, `emit`, `on`, `run`. Pass `ctx.signal` to `fetch`.
|
|
10
10
|
- Effects take `ctx` as the first argument: `retry(ctx, fn, options)`, `timeout(ctx, ms, fn)`, `take(ctx, "event")`.
|
|
11
11
|
- Policies wrap a handler: `bus.on("search", latest(async (query, ctx) => { ... }))`.
|
|
12
|
-
- Each part is imported from its own path: `nanoeffects` (`createBus`), `nanoeffects/policies`, `nanoeffects/effects`, `nanoeffects/actions`, `nanoeffects/redux`, `nanoeffects/observable`, `nanoeffects/react` (`reactPlugin`, adds `bus.useEmitter`; `usePolicy`; needs React), `nanoeffects/trace` (`createTracer`, loggers, `tracePlugin`), `nanoeffects/dev`.
|
|
12
|
+
- Each part is imported from its own path: `nanoeffects` (`createBus`), `nanoeffects/policies`, `nanoeffects/effects`, `nanoeffects/actions`, `nanoeffects/redux`, `nanoeffects/observable`, `nanoeffects/react` (`reactPlugin`, adds `bus.useEmitter` and `bus.useEventState`; `usePolicy`; needs React), `nanoeffects/trace` (`createTracer`, loggers, `tracePlugin`), `nanoeffects/dev`.
|
|
13
13
|
- `bus.emit(command(payload))` runs the command's handler and returns its result. Plain events return nothing.
|
|
14
14
|
|
|
15
15
|
---
|
|
@@ -500,6 +500,87 @@ More in [Errors](errors.md).
|
|
|
500
500
|
| Guarantees | [Semantics](semantics.md) |
|
|
501
501
|
| Which import | [Package entries](package.md) |
|
|
502
502
|
|
|
503
|
+
---
|
|
504
|
+
Source: docs/starter.md
|
|
505
|
+
|
|
506
|
+
# Start a project from a template
|
|
507
|
+
|
|
508
|
+
Use this page when you want a running project in one command instead of wiring Vite and the bus by hand. It makes a small app with a search box that waits for you to stop typing (`debounce`) and aborts the older request when a newer one starts (`latest`).
|
|
509
|
+
|
|
510
|
+
You need Node 18 or newer.
|
|
511
|
+
|
|
512
|
+
## Create the project
|
|
513
|
+
|
|
514
|
+
Run the command for your package manager. Replace `my-app` with the name of the new directory.
|
|
515
|
+
|
|
516
|
+
```bash
|
|
517
|
+
npm create nanoeffects@latest my-app
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
```bash
|
|
521
|
+
pnpm create nanoeffects my-app
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
```bash
|
|
525
|
+
yarn create nanoeffects my-app
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
```bash
|
|
529
|
+
bun create nanoeffects my-app
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
The command asks for the template when you do not give one. The directory must be new or empty: the command stops instead of overwriting files.
|
|
533
|
+
|
|
534
|
+
Then follow the steps it prints:
|
|
535
|
+
|
|
536
|
+
```bash
|
|
537
|
+
cd my-app
|
|
538
|
+
npm install
|
|
539
|
+
npm run dev
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Open the address that Vite prints and type in the box. The fake server answers after 800 ms, so you can see the older request being aborted.
|
|
543
|
+
|
|
544
|
+
## Choose a template
|
|
545
|
+
|
|
546
|
+
Pass `--template` to skip the question. With npm, put `--` before the option.
|
|
547
|
+
|
|
548
|
+
| Template | What it shows | Use it when |
|
|
549
|
+
| -------- | ------------- | ----------- |
|
|
550
|
+
| `vanilla` | A bus, one event and a `debounce` plus `latest` handler, with plain DOM code | You use no framework, or want to see the bus alone |
|
|
551
|
+
| `react` | The same handler with `useEmitter` and `useEventState`, and `usePolicy` for a debounced save in a component | You build a React app |
|
|
552
|
+
| `vanilla-ts` | The `vanilla` app in TypeScript | You want the compiler to check events and handlers |
|
|
553
|
+
| `react-ts` | The `react` app in TypeScript | You build a React app in TypeScript |
|
|
554
|
+
|
|
555
|
+
```bash
|
|
556
|
+
npm create nanoeffects@latest my-app -- --template react
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
The TypeScript templates are the same apps with the same files, written in `.ts` and `.tsx`. Events are made with `createAction<Payload>`, so handlers and `useEventState` get their types from the events, with no annotation in the component. `npm run build` type-checks the project (`tsc --noEmit`) before bundling it.
|
|
560
|
+
|
|
561
|
+
```bash
|
|
562
|
+
npm create nanoeffects@latest my-app -- --template react-ts
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
## Read the files
|
|
566
|
+
|
|
567
|
+
Every template splits the code the way [the guide](guide.md) does: one job per file. In the TypeScript templates the files end in `.ts` (and `.tsx` for components).
|
|
568
|
+
|
|
569
|
+
| File | Job |
|
|
570
|
+
| ---- | --- |
|
|
571
|
+
| `src/bus.js` | Creates the bus. It imports no events. |
|
|
572
|
+
| `src/search.js` | Names the events with `createAction`. It imports no bus. |
|
|
573
|
+
| `src/search-handlers.js` | Handles the events with `debounce` and `latest`. It knows the bus and the events. |
|
|
574
|
+
| `src/api.js` | A fake server. Replace it with your own requests. |
|
|
575
|
+
|
|
576
|
+
Every call of nanoeffects in these files has a comment that says what it does. To make it your own, replace `src/api.js`, then rename the events and the handler.
|
|
577
|
+
|
|
578
|
+
## Next steps
|
|
579
|
+
|
|
580
|
+
- [Policies](policies.md): `leading`, `throttle` and `queue` next to `latest`.
|
|
581
|
+
- [Build a sign-up form in React](react.md): the same ideas in a bigger React example.
|
|
582
|
+
- [Package entries](package.md): which import holds what.
|
|
583
|
+
|
|
503
584
|
---
|
|
504
585
|
Source: docs/motivation.md
|
|
505
586
|
|
|
@@ -3041,6 +3122,68 @@ const checkName = latest(async (name, ctx) => {
|
|
|
3041
3122
|
|
|
3042
3123
|
In the component, add `bus.emit("form/closed")` to the cleanup function of the effect. This is optional: skip it when finishing a request after unmount does no harm, as for a cheap check.
|
|
3043
3124
|
|
|
3125
|
+
## Show the last value of an event as state
|
|
3126
|
+
|
|
3127
|
+
**Problem.** A component often needs the latest result of an event, such as the found items of a search. The hand-written form is easy to get slightly wrong:
|
|
3128
|
+
|
|
3129
|
+
<!-- docs-check: skip - the hand-written form that this section replaces -->
|
|
3130
|
+
```jsx
|
|
3131
|
+
useEffect(() => bus.on(searchLoaded, setFound), []);
|
|
3132
|
+
```
|
|
3133
|
+
|
|
3134
|
+
It needs a `useState` next to it, the empty `[]` hides the dependency on the event, and it breaks if the payload is a function, because React calls `setFound(payload)` as an updater.
|
|
3135
|
+
|
|
3136
|
+
**When.** The value you show comes from the bus, not from the component. For the opposite direction, a value of the component that the bus must hear, use `useEmitter`.
|
|
3137
|
+
|
|
3138
|
+
`useEventState(event, initial)` has the signature of `useState`: it returns `[state, setState]`. `initial` is the state before the first event and may be a function, as in `useState`. After that, every emit of `event` sets the state to its payload. The hook unsubscribes when the component unmounts.
|
|
3139
|
+
|
|
3140
|
+
```js
|
|
3141
|
+
// bus.js
|
|
3142
|
+
import { createBus } from "nanoeffects";
|
|
3143
|
+
import { reactPlugin } from "nanoeffects/react";
|
|
3144
|
+
|
|
3145
|
+
// createBus — the event bus, created once outside React
|
|
3146
|
+
// reactPlugin — adds the hooks bus.useEmitter and bus.useEventState
|
|
3147
|
+
export const bus = createBus().use(reactPlugin());
|
|
3148
|
+
export const { useEventState } = bus; // a free hook: components import it without the bus
|
|
3149
|
+
```
|
|
3150
|
+
|
|
3151
|
+
```js
|
|
3152
|
+
// search.js
|
|
3153
|
+
import { createAction } from "nanoeffects/actions";
|
|
3154
|
+
|
|
3155
|
+
// createAction — the event name; the handlers emit it with the found titles
|
|
3156
|
+
export const searchLoaded = createAction("search/loaded");
|
|
3157
|
+
```
|
|
3158
|
+
|
|
3159
|
+
```jsx
|
|
3160
|
+
// Found.jsx
|
|
3161
|
+
import { useEventState } from "./bus";
|
|
3162
|
+
import { searchLoaded } from "./search";
|
|
3163
|
+
|
|
3164
|
+
export function Found() {
|
|
3165
|
+
// useEventState — like useState([]), but searchLoaded sets the value
|
|
3166
|
+
const [found, setFound] = useEventState(searchLoaded, []);
|
|
3167
|
+
|
|
3168
|
+
return (
|
|
3169
|
+
<>
|
|
3170
|
+
<ul>
|
|
3171
|
+
{found.map((title) => (
|
|
3172
|
+
<li key={title}>{title}</li>
|
|
3173
|
+
))}
|
|
3174
|
+
</ul>
|
|
3175
|
+
<button onClick={() => setFound([])}>Clear</button>
|
|
3176
|
+
</>
|
|
3177
|
+
);
|
|
3178
|
+
}
|
|
3179
|
+
```
|
|
3180
|
+
|
|
3181
|
+
`setFound` is the setter of React. It changes the state of this component only and emits nothing.
|
|
3182
|
+
|
|
3183
|
+
In TypeScript, the state type comes from the event: from `createAction<string[]>("search/loaded")` or from the event map of the bus. When the event has no payload type, as in the example above, it comes from `initial`: `["a"]` is `string[]`, and an empty `[]` is `unknown[]`. Give the event a type, or pass a typed `initial`, when you need more.
|
|
3184
|
+
|
|
3185
|
+
The bus keeps no values, so the hook cannot replay an event. An event emitted before the component mounts is not seen: if that matters, read the current value into `initial`.
|
|
3186
|
+
|
|
3044
3187
|
## Keep a debounced handler between renders
|
|
3045
3188
|
|
|
3046
3189
|
**Problem.** A component runs again on every render. A policy made in its body, such as `debounce(300, search)`, is made again each time and loses its timer. A policy made once keeps the callback of the first render, which reads old props and state.
|
package/llms.txt
CHANGED
|
@@ -9,12 +9,13 @@ Rules that generated code often gets wrong:
|
|
|
9
9
|
- A handler gets the payload and a task context: `(payload, ctx)`. The context has `signal`, `emit`, `on`, `run`. Pass `ctx.signal` to `fetch`.
|
|
10
10
|
- Effects take `ctx` as the first argument: `retry(ctx, fn, options)`, `timeout(ctx, ms, fn)`, `take(ctx, "event")`.
|
|
11
11
|
- Policies wrap a handler: `bus.on("search", latest(async (query, ctx) => { ... }))`.
|
|
12
|
-
- Each part is imported from its own path: `nanoeffects` (`createBus`), `nanoeffects/policies`, `nanoeffects/effects`, `nanoeffects/actions`, `nanoeffects/redux`, `nanoeffects/observable`, `nanoeffects/react` (`reactPlugin`, adds `bus.useEmitter`; `usePolicy`; needs React), `nanoeffects/trace` (`createTracer`, loggers, `tracePlugin`), `nanoeffects/dev`.
|
|
12
|
+
- Each part is imported from its own path: `nanoeffects` (`createBus`), `nanoeffects/policies`, `nanoeffects/effects`, `nanoeffects/actions`, `nanoeffects/redux`, `nanoeffects/observable`, `nanoeffects/react` (`reactPlugin`, adds `bus.useEmitter` and `bus.useEventState`; `usePolicy`; needs React), `nanoeffects/trace` (`createTracer`, loggers, `tracePlugin`), `nanoeffects/dev`.
|
|
13
13
|
- `bus.emit(command(payload))` runs the command's handler and returns its result. Plain events return nothing.
|
|
14
14
|
|
|
15
15
|
## Start here
|
|
16
16
|
|
|
17
17
|
- [Guide](docs/guide.md): bus, handler context, policies, effects, commands and modules, step by step
|
|
18
|
+
- [Start a project from a template](docs/starter.md): `npm create nanoeffects` makes a Vite project (`vanilla` or `react`) with a bus, `debounce` and `latest`
|
|
18
19
|
- [Why nanoeffects exists](docs/motivation.md): no dependencies and no global state for SDKs and Module Federation, and why not Redux, Effector or RxJS
|
|
19
20
|
- [Package entries](docs/package.md): which import holds what
|
|
20
21
|
- [Plugins](docs/plugins.md): every plugin of a bus, what `bus.use` returns, and the order to use them in
|
|
@@ -33,7 +34,7 @@ Rules that generated code often gets wrong:
|
|
|
33
34
|
- [Actions, commands, jobs and modules](docs/actions.md): `createAction`, `createCommand`, `createJob`, `createModule`
|
|
34
35
|
- [Connect Redux, Zustand, MobX, or Pinia](docs/stores.md): the Redux middleware and callbacks of other stores
|
|
35
36
|
- [Use nanoeffects with React Query and SWR](docs/query-libraries.md): call a command from `queryFn` or the SWR fetcher; `signalPlugin` passes the abort of the library to the task
|
|
36
|
-
- [Build a sign-up form in React](docs/react.md): a form with a username check, a submit that cannot fire twice, and retry; `usePolicy` keeps a debounced handler between renders
|
|
37
|
+
- [Build a sign-up form in React](docs/react.md): a form with a username check, a submit that cannot fire twice, and retry; `useEventState` shows an event as state, `usePolicy` keeps a debounced handler between renders
|
|
37
38
|
- [Context fields](docs/context.md): put a store or client on every task
|
|
38
39
|
- [Scopes](docs/scope.md): fields for one command, action, job, module or `emit`, and a way to replace them in a test
|
|
39
40
|
- [Use the bus from a class](docs/oop.md): `Emitter`, `extendBus`, `@WithBus`, `@On`, `@Once`, and `dispose()` for the subscriptions of an instance
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nanoeffects",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Cancel stale async work and fix race conditions in event handlers: a typed event bus where every handler is a task with an AbortSignal, plus latest, debounce, throttle, queue, retry, timeout and poll. About 1.4 kB gzip.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"event-bus",
|
|
@@ -138,16 +138,17 @@
|
|
|
138
138
|
"scripts": {
|
|
139
139
|
"build": "tsdown && node scripts/notrace-types.mjs && node scripts/llms-full.mjs",
|
|
140
140
|
"prepublishOnly": "pnpm build",
|
|
141
|
-
"check": "biome check ./src ./examples ./integration ./scripts",
|
|
142
|
-
"format": "npx @biomejs/biome check --write --unsafe ./src ./examples ./integration ./scripts",
|
|
141
|
+
"check": "biome check ./src ./examples ./integration ./scripts ./packages",
|
|
142
|
+
"format": "npx @biomejs/biome check --write --unsafe ./src ./examples ./integration ./scripts ./packages",
|
|
143
143
|
"tsgo": "tsc --noEmit",
|
|
144
144
|
"test": "vitest run",
|
|
145
145
|
"test:watch": "vitest",
|
|
146
146
|
"prepare": "lefthook install",
|
|
147
|
-
"lint:package": "publint && attw --pack . --profile esm-only",
|
|
147
|
+
"lint:package": "publint && attw --pack . --profile esm-only && publint packages/create-nanoeffects",
|
|
148
148
|
"size": "size-limit",
|
|
149
|
-
"verify": "pnpm tsgo && pnpm check && pnpm docs:check && pnpm llms:check && pnpm test && pnpm build && pnpm test:integration && pnpm smoke && pnpm lint:package && pnpm size",
|
|
149
|
+
"verify": "pnpm tsgo && pnpm check && pnpm docs:check && pnpm llms:check && pnpm templates:check && pnpm test && pnpm build && pnpm test:integration && pnpm smoke && pnpm lint:package && pnpm size",
|
|
150
150
|
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
151
|
+
"templates:check": "node scripts/templates-check.mjs",
|
|
151
152
|
"docs:check": "node scripts/docs-check.mjs",
|
|
152
153
|
"llms:check": "node scripts/llms-full.mjs --check",
|
|
153
154
|
"smoke": "node scripts/smoke.mjs && node scripts/smoke.mjs --notrace",
|
|
@@ -163,6 +164,7 @@
|
|
|
163
164
|
"@size-limit/file": "^14.2.0",
|
|
164
165
|
"@types/react": "^19.3.0",
|
|
165
166
|
"@types/react-dom": "^19.3.0",
|
|
167
|
+
"@vitejs/plugin-react": "^6.1.2",
|
|
166
168
|
"archunit": "^2.5.4",
|
|
167
169
|
"happy-dom": "^21.0.4",
|
|
168
170
|
"jotai": "^3.0.1",
|
|
@@ -249,7 +251,7 @@
|
|
|
249
251
|
"ignore": [
|
|
250
252
|
"react"
|
|
251
253
|
],
|
|
252
|
-
"limit": "1.
|
|
254
|
+
"limit": "1.24 kB"
|
|
253
255
|
},
|
|
254
256
|
{
|
|
255
257
|
"name": "dev",
|