nanoeffects 0.7.0 → 0.9.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 -2
- package/dist/{action-creator-Bd4TBZgl.d.ts → action-creator-BrZF6hXc.d.ts} +0 -4
- package/dist/actions.d.ts +41 -25
- package/dist/actions.js +1 -1
- package/dist/bus-CkTHUAvq.js +1 -0
- package/dist/context-COgyNlrC.js +1 -0
- package/dist/{context-plugin-BVNKqkIx.d.ts → context-plugin--HkNN1ha.d.ts} +1 -1
- package/dist/dev.d.ts +9 -8
- package/dist/dev.js +1 -1
- package/dist/effects.d.ts +2 -2
- package/dist/effects.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/match.d.ts +50 -0
- package/dist/match.js +1 -0
- package/dist/{module-DZ5ngUrM.d.ts → module-DqKegXHt.d.ts} +43 -18
- package/dist/notrace/{action-creator-D-y_-xRS.d.ts → action-creator-DHAEgwuh.d.ts} +0 -4
- package/dist/notrace/actions.d.ts +90 -39
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/bus-BmCTo8BG.js +1 -0
- package/dist/notrace/{context-plugin-DDfsaErA.d.ts → context-plugin-BapQkGwQ.d.ts} +1 -1
- package/dist/notrace/context-yYU3_9lq.js +1 -0
- package/dist/notrace/{delay-B_W1tnkS.js → delay-BosJKFjg.js} +1 -1
- package/dist/notrace/effects.d.ts +2 -2
- package/dist/notrace/effects.js +1 -1
- package/dist/notrace/index.d.ts +2 -2
- package/dist/notrace/index.js +1 -1
- package/dist/notrace/{plugin-CDAngHwg.d.ts → plugin-Cxadz6uU.d.ts} +2 -2
- package/dist/notrace/policies.d.ts +30 -2
- package/dist/notrace/policies.js +1 -1
- package/dist/notrace/redux.d.ts +1 -1
- package/dist/observable.d.ts +1 -1
- package/dist/oop.d.ts +131 -0
- package/dist/oop.js +1 -0
- package/dist/{plugin-DQNPBcxx.d.ts → plugin-DkXCvsmB.d.ts} +2 -2
- package/dist/policies.d.ts +3 -2
- package/dist/policies.js +1 -1
- package/dist/react.d.ts +33 -1
- package/dist/react.js +1 -1
- package/dist/redux.d.ts +1 -1
- package/dist/scope.d.ts +5 -6
- package/dist/scope.js +1 -1
- package/dist/signal.d.ts +51 -0
- package/dist/signal.js +1 -0
- package/dist/trace.d.ts +1 -1
- package/dist/{types-BJ0fzV1p.d.ts → types-KavbvHix.d.ts} +13 -3
- package/dist/with-policy-CqUS-VWW.d.ts +30 -0
- package/dist/with-policy-DBV9dh2F.js +1 -0
- package/docs/README.md +7 -0
- package/docs/actions.md +152 -39
- package/docs/cancel-stale-requests.md +111 -0
- package/docs/debounce-and-abort.md +106 -0
- package/docs/guide.md +143 -19
- package/docs/hmr.md +6 -5
- package/docs/migrating-from-effector.md +51 -18
- package/docs/migrating-from-mitt.md +97 -0
- package/docs/migrating-from-rxjs.md +17 -6
- package/docs/motivation.md +41 -0
- package/docs/observable.md +124 -10
- package/docs/oop.md +286 -0
- package/docs/package.md +6 -2
- package/docs/plugins.md +247 -51
- package/docs/policies.md +33 -0
- package/docs/prevent-double-submit.md +128 -0
- package/docs/query-libraries.md +124 -0
- package/docs/react.md +61 -5
- package/docs/scope.md +60 -22
- package/docs/stores.md +263 -109
- package/llms-full.txt +2153 -355
- package/llms.txt +10 -3
- package/package.json +43 -4
- package/skills/nanoeffects/SKILL.md +52 -0
- package/dist/bus-BAs9x7IQ.js +0 -1
- package/dist/context-CK0ZomWt.js +0 -1
- package/dist/notrace/bus-Bdk0YyFQ.js +0 -1
- package/dist/notrace/context-CctVmHLC.js +0 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Debounce a request and abort the previous one
|
|
2
|
+
|
|
3
|
+
Read this page if you debounce user input (a search box, an autosave, a resize handler) and you also want the request in flight to stop when a new one starts. `debounce` alone waits for a pause. It does not cancel a request that already started. This page shows the two policies together, the order to put them in, and when to use `throttle` instead.
|
|
4
|
+
|
|
5
|
+
## Know what each policy does
|
|
6
|
+
|
|
7
|
+
| Policy | What it does | What it does not do |
|
|
8
|
+
| ------ | ------------ | ------------------- |
|
|
9
|
+
| `debounce(ms, fn)` | runs `fn` after `ms` without a new call | does not cancel a run that already started |
|
|
10
|
+
| `latest(fn)` | aborts the previous run when a new one starts | does not wait for a pause |
|
|
11
|
+
|
|
12
|
+
Use both for a search box. `debounce` sends fewer requests. `latest` cancels the slow one when the user types again after the pause.
|
|
13
|
+
|
|
14
|
+
## Combine them
|
|
15
|
+
|
|
16
|
+
Put `debounce` outside and `latest` inside. The event waits for a pause first. When the run starts, it aborts the previous run.
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import { createBus } from "nanoeffects";
|
|
20
|
+
import { debounce, latest } from "nanoeffects/policies";
|
|
21
|
+
|
|
22
|
+
const bus = createBus(); // the bus: events and handlers
|
|
23
|
+
|
|
24
|
+
// latest: a newer run aborts the older request
|
|
25
|
+
const searchLatest = latest(async (query, { signal }) => {
|
|
26
|
+
// signal: pass it on so the browser cancels the request itself
|
|
27
|
+
const items = await api.search(query, { signal });
|
|
28
|
+
bus.emit("results", items);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// debounce: run searchLatest after 300 ms without a new "typed"
|
|
32
|
+
bus.on("typed", debounce(300, searchLatest));
|
|
33
|
+
|
|
34
|
+
bus.on("results", (items) => render(items));
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
What to replace: `api.search` is your own request function. It must accept `{ signal }` and give it to `fetch`. `render` shows the items.
|
|
38
|
+
|
|
39
|
+
Result: typing `a`, `ab`, `abc` within 300 ms sends one request for `abc`. If the user types `abcd` while that request runs, the request for `abc` is aborted and one for `abcd` starts after the pause.
|
|
40
|
+
|
|
41
|
+
## Run the first call at once
|
|
42
|
+
|
|
43
|
+
By default `debounce` waits even before the first call. For an input where the first key press should react at once, add `leading: true`. The first call of a burst runs immediately, and the last one runs after the pause.
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
import { createBus } from "nanoeffects";
|
|
47
|
+
import { debounce } from "nanoeffects/policies";
|
|
48
|
+
|
|
49
|
+
const bus = createBus(); // the bus: events and handlers
|
|
50
|
+
|
|
51
|
+
const showSuggestions = (query) => console.log("suggest", query);
|
|
52
|
+
|
|
53
|
+
// debounce with leading: the first call runs at once, the last one after 300 ms of quiet
|
|
54
|
+
bus.on("typed", debounce(300, showSuggestions, { leading: true }));
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`leading` and `trailing` follow lodash. All combinations: [Policies](policies.md#configure-debounce-and-throttle).
|
|
58
|
+
|
|
59
|
+
## Use throttle for continuous input
|
|
60
|
+
|
|
61
|
+
`debounce` never runs while calls keep coming. For a scroll or a drag you want updates during the movement. Use `throttle`: at most one run per period.
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
import { createBus } from "nanoeffects";
|
|
65
|
+
import { throttle } from "nanoeffects/policies";
|
|
66
|
+
|
|
67
|
+
const bus = createBus(); // the bus: events and handlers
|
|
68
|
+
|
|
69
|
+
const saveScroll = (position) => localStorage.setItem("scroll", String(position));
|
|
70
|
+
|
|
71
|
+
// throttle: at most one run per 100 ms while "scrolled" keeps coming
|
|
72
|
+
bus.on("scrolled", throttle(100, saveScroll));
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Debounce without a bus
|
|
76
|
+
|
|
77
|
+
If the page has no bus, wrap the same pair with `withPolicy`. The result is a plain function that you call from an input handler.
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
import { debounce, latest, withPolicy } from "nanoeffects/policies";
|
|
81
|
+
|
|
82
|
+
// latest: a newer run aborts the older request
|
|
83
|
+
const searchLatest = latest(async (query, { signal }) => {
|
|
84
|
+
const items = await api.search(query, { signal });
|
|
85
|
+
console.log(items);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// withPolicy: debounce by 300 ms, called as a plain function
|
|
89
|
+
const onTyped = withPolicy(debounce(300, searchLatest));
|
|
90
|
+
|
|
91
|
+
onTyped("a");
|
|
92
|
+
onTyped("ab"); // only "ab" runs, 300 ms later
|
|
93
|
+
|
|
94
|
+
// cancel: drop a pending call and abort a running request, for example on page leave
|
|
95
|
+
onTyped.cancel();
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
In a React component use [`usePolicy`](react.md#keep-a-debounced-handler-between-renders), which also cancels on unmount.
|
|
99
|
+
|
|
100
|
+
## Check these when it does not cancel
|
|
101
|
+
|
|
102
|
+
- The request did not get `signal`. `latest` then drops the old result but the request goes on.
|
|
103
|
+
- The order is reversed. Write `debounce(300, latest(handler))`, as above, so `debounce` receives the events and `latest` guards the request.
|
|
104
|
+
- Several inputs share one event. Give each its own event name, or use `{ key }`, for example `latest(handler, { key: (input) => input.id })`.
|
|
105
|
+
|
|
106
|
+
Related: [Cancel stale requests](cancel-stale-requests.md), [Policies](policies.md).
|
package/docs/guide.md
CHANGED
|
@@ -69,6 +69,74 @@ unbind();
|
|
|
69
69
|
- An event that only `"*"` listens to counts as heard, so `onUnhandled` does not fire for it.
|
|
70
70
|
- `bus.off("*")` removes every `"*"` listener; `bus.off()` clears the whole bus, `"*"` included.
|
|
71
71
|
|
|
72
|
+
### Pick events with a function
|
|
73
|
+
|
|
74
|
+
To listen to a group of events, such as every `user/*` event, `"*"` and a check of `ctx.type` do the job but leave the check to you. `matchPlugin` lets `on` take a function that picks the events. The function gets `{ type, payload }` of each event, and the handler runs when it returns true.
|
|
75
|
+
|
|
76
|
+
<!-- docs-check: new -->
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
// bus.js
|
|
80
|
+
import { createBus } from "nanoeffects";
|
|
81
|
+
import { matchPlugin } from "nanoeffects/match";
|
|
82
|
+
|
|
83
|
+
// matchPlugin — lets on, once and off take a function that picks the events
|
|
84
|
+
export const bus = createBus().use(matchPlugin());
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
// user.js
|
|
89
|
+
import { createAction } from "nanoeffects/actions";
|
|
90
|
+
|
|
91
|
+
export const userLoaded = createAction("user/loaded");
|
|
92
|
+
export const userFailed = createAction("user/failed");
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
// user-handlers.js
|
|
97
|
+
import { bus } from "./bus";
|
|
98
|
+
|
|
99
|
+
// the function picks the events; the handler gets the payload and the context of each
|
|
100
|
+
bus.on(
|
|
101
|
+
(action) => action.type.startsWith("user/"),
|
|
102
|
+
(payload, ctx) => {
|
|
103
|
+
console.log(ctx.type, payload);
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
import { bus } from "./bus";
|
|
110
|
+
import { userFailed, userLoaded } from "./user";
|
|
111
|
+
|
|
112
|
+
bus.emit(userLoaded({ id: "1" })); // user/loaded { id: "1" }
|
|
113
|
+
bus.emit("order/placed", 5); // not picked
|
|
114
|
+
bus.emit(userFailed("boom")); // user/failed boom
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- `once(fn, handler)` waits for the first event that `fn` accepts. `off(fn)` removes every handler of `fn`, and `off(fn, handler)` removes one.
|
|
118
|
+
- An action creator or a command still means that one event: only a function with no `type` is a function that picks events.
|
|
119
|
+
- The cost: the handler listens to `"*"`, so each event makes one task for each such subscription, also when the function says no, and an event that only such a subscription listens to counts as heard for `onUnhandled`. For a few known events, `on(creator, handler)` is cheaper.
|
|
120
|
+
- It works in any order with the other plugins, `devPlugin` included.
|
|
121
|
+
|
|
122
|
+
In TypeScript a function that is a type guard narrows the payload of the handler:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { bus } from "./bus";
|
|
126
|
+
|
|
127
|
+
type User = { id: string };
|
|
128
|
+
|
|
129
|
+
// the guard says which event this is, and the handler gets its payload typed
|
|
130
|
+
const isLoaded = (action: {
|
|
131
|
+
type: string;
|
|
132
|
+
payload: unknown;
|
|
133
|
+
}): action is { type: "user/loaded"; payload: User } => action.type === "user/loaded";
|
|
134
|
+
|
|
135
|
+
bus.on(isLoaded, (user) => console.log(user.id)); // user: User
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`ctx.on` takes such a function at run time too, but its type does not know it yet.
|
|
139
|
+
|
|
72
140
|
## 2. Use the handler context
|
|
73
141
|
|
|
74
142
|
The second argument is the **task** context for this call. Emit further events through it. Pass `ctx.signal` to `fetch` when you cancel work.
|
|
@@ -149,7 +217,7 @@ const searchLatest = latest(async (query, { signal }) => {
|
|
|
149
217
|
bus.on("search", debounce(300, searchLatest));
|
|
150
218
|
```
|
|
151
219
|
|
|
152
|
-
Full options: [Policies](policies.md).
|
|
220
|
+
Full options: [Policies](policies.md). No bus? A policy also works as a plain function: [Use a policy without a bus](policies.md#use-a-policy-without-a-bus).
|
|
153
221
|
|
|
154
222
|
## 5. Add retry and timeout
|
|
155
223
|
|
|
@@ -225,22 +293,42 @@ More in [Effects](effects.md).
|
|
|
225
293
|
|
|
226
294
|
`emit` and `on` take a string, an object `{ type, payload }` (a Redux action fits), or a function with a `type` property (action creators and commands).
|
|
227
295
|
|
|
296
|
+
From here on the bus is kept in one file and the events in files named after their domain (`move.js`, `math.js`), as in a real app: the events can then be imported anywhere without importing the bus.
|
|
297
|
+
|
|
228
298
|
```js
|
|
299
|
+
// bus.js
|
|
229
300
|
import { createBus } from "nanoeffects";
|
|
230
|
-
import { createAction, createCommand } from "nanoeffects/actions";
|
|
231
301
|
|
|
232
|
-
const bus = createBus();
|
|
302
|
+
export const bus = createBus();
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
```js
|
|
306
|
+
// move.js
|
|
307
|
+
import { createAction } from "nanoeffects/actions";
|
|
308
|
+
|
|
309
|
+
// createAction — names an event; calling it builds { type, payload }
|
|
310
|
+
export const moved = createAction("moved");
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
```js
|
|
314
|
+
// math.js
|
|
315
|
+
import { createCommand } from "nanoeffects/actions";
|
|
316
|
+
|
|
317
|
+
// createCommand — like an action, but emit runs a handler and returns its result
|
|
318
|
+
export const double = createCommand("math/double", (value) => value * 2);
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
```js
|
|
322
|
+
import { bus } from "./bus";
|
|
323
|
+
import { moved } from "./move";
|
|
324
|
+
import { double } from "./math";
|
|
233
325
|
|
|
234
326
|
bus.emit("moved", { x: 1, y: 2 });
|
|
235
327
|
bus.emit({ type: "moved", payload: { x: 2, y: 2 } });
|
|
236
328
|
|
|
237
|
-
// createAction — names an event; calling it builds { type, payload }
|
|
238
|
-
const moved = createAction("moved");
|
|
239
329
|
bus.emit(moved({ x: 3, y: 2 }));
|
|
240
330
|
bus.on(moved, ({ x }) => console.log(x));
|
|
241
331
|
|
|
242
|
-
// createCommand — like an action, but emit runs a handler and returns its result
|
|
243
|
-
const double = createCommand("math/double", (value) => value * 2);
|
|
244
332
|
bus.emit(double(21)); // 42
|
|
245
333
|
```
|
|
246
334
|
|
|
@@ -251,14 +339,25 @@ Use strings for the plain case. Use objects when other code already builds actio
|
|
|
251
339
|
A command may carry its handler, or only be a **contract** (name + types) with the work registered elsewhere — another file, or a fake in a test.
|
|
252
340
|
|
|
253
341
|
```js
|
|
254
|
-
|
|
255
|
-
import { createCommand
|
|
342
|
+
// user.js
|
|
343
|
+
import { createCommand } from "nanoeffects/actions";
|
|
256
344
|
|
|
257
|
-
const getUser = createCommand("user/get"); // contract — no handler yet
|
|
345
|
+
export const getUser = createCommand("user/get"); // contract — no handler yet
|
|
346
|
+
```
|
|
258
347
|
|
|
259
|
-
|
|
348
|
+
```js
|
|
349
|
+
// bus.js
|
|
350
|
+
import { createBus } from "nanoeffects";
|
|
351
|
+
import { commandsPlugin } from "nanoeffects/actions";
|
|
352
|
+
|
|
353
|
+
export const bus = createBus();
|
|
260
354
|
// commandsPlugin — registry of handlers for this bus
|
|
261
|
-
const commands = bus.use(commandsPlugin());
|
|
355
|
+
export const commands = bus.use(commandsPlugin());
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
```js
|
|
359
|
+
import { bus, commands } from "./bus";
|
|
360
|
+
import { getUser } from "./user";
|
|
262
361
|
|
|
263
362
|
bus.emit(getUser("1")); // throws: No handler for command "user/get"
|
|
264
363
|
|
|
@@ -279,16 +378,27 @@ Each bus has its own registry. A [module](#9-mount-a-domain-as-a-module) can reg
|
|
|
279
378
|
**What you do.** `createModule(name, setup)` describes the feature. `bus.use(modulePlugin([…]))` mounts it. `app.unmount(module)` undoes everything the setup registered.
|
|
280
379
|
|
|
281
380
|
```js
|
|
282
|
-
|
|
283
|
-
import { createCommand
|
|
381
|
+
// user.js
|
|
382
|
+
import { createCommand } from "nanoeffects/actions";
|
|
284
383
|
|
|
285
|
-
const getUser = createCommand("user/get"); // contract — no handler yet
|
|
286
|
-
|
|
384
|
+
export const getUser = createCommand("user/get"); // contract — no handler yet
|
|
385
|
+
```
|
|
287
386
|
|
|
288
|
-
|
|
387
|
+
```js
|
|
388
|
+
// math.js
|
|
389
|
+
import { createCommand } from "nanoeffects/actions";
|
|
390
|
+
|
|
391
|
+
export const double = createCommand("math/double", (value) => value * 2); // carries its handler
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
```js
|
|
395
|
+
// greeting-module.js
|
|
396
|
+
import { createModule } from "nanoeffects/actions";
|
|
397
|
+
import { getUser } from "./user";
|
|
398
|
+
import { double } from "./math";
|
|
289
399
|
|
|
290
400
|
// createModule — one feature; name is stable for hot reload replace
|
|
291
|
-
const greeting = createModule("greeting", (m) => {
|
|
401
|
+
export const greeting = createModule("greeting", (m) => {
|
|
292
402
|
// m.on — like bus.on, removed automatically on unmount
|
|
293
403
|
m.on("greet", (name) => console.log(`hello ${name}`));
|
|
294
404
|
|
|
@@ -298,9 +408,23 @@ const greeting = createModule("greeting", (m) => {
|
|
|
298
408
|
// m.use — register a command that already has a handler, for this module’s life
|
|
299
409
|
m.use(double);
|
|
300
410
|
});
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
```js
|
|
414
|
+
// bus.js
|
|
415
|
+
import { createBus } from "nanoeffects";
|
|
416
|
+
import { modulePlugin } from "nanoeffects/actions";
|
|
417
|
+
import { greeting } from "./greeting-module";
|
|
301
418
|
|
|
419
|
+
export const bus = createBus();
|
|
302
420
|
// modulePlugin — mount modules on the bus
|
|
303
|
-
const app = bus.use(modulePlugin([greeting]));
|
|
421
|
+
export const app = bus.use(modulePlugin([greeting]));
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
```js
|
|
425
|
+
import { app, bus } from "./bus";
|
|
426
|
+
import { greeting } from "./greeting-module";
|
|
427
|
+
|
|
304
428
|
bus.emit("greet", "Ada"); // hello Ada
|
|
305
429
|
app.unmount(greeting); // greet listener and getUser binding are gone
|
|
306
430
|
```
|
package/docs/hmr.md
CHANGED
|
@@ -21,7 +21,7 @@ A `bus.on` outside a module returns the function that unsubscribes: hand it to t
|
|
|
21
21
|
|
|
22
22
|
## Report leaks with `devPlugin`
|
|
23
23
|
|
|
24
|
-
`bus.use(devPlugin(
|
|
24
|
+
`bus.use(devPlugin())` reports what is easy to miss while developing:
|
|
25
25
|
|
|
26
26
|
```js
|
|
27
27
|
import { createBus } from "nanoeffects";
|
|
@@ -29,13 +29,14 @@ import { modulePlugin } from "nanoeffects/actions";
|
|
|
29
29
|
import { devPlugin } from "nanoeffects/dev";
|
|
30
30
|
|
|
31
31
|
const bus = createBus();
|
|
32
|
-
|
|
32
|
+
bus.use(modulePlugin([userModule]));
|
|
33
33
|
// devPlugin — warn on piled-up listeners; log module mount/replace/unmount
|
|
34
|
-
|
|
34
|
+
// (it finds the app of modulePlugin on the bus, in either order of the two)
|
|
35
|
+
bus.use(devPlugin());
|
|
35
36
|
```
|
|
36
37
|
|
|
37
38
|
- **Listeners that pile up.** When more than `maxListeners` (default `10`) listeners of one event were added and not unsubscribed, one warning names the event and the count. The count follows the function that `on` returns; `bus.off(event, fn)` is not seen.
|
|
38
|
-
- **Modules.**
|
|
39
|
+
- **Modules.** For the app of `modulePlugin` (or the `app` option) it logs `module "user" mounted`, `replaced` when a module of the same name was mounted, and `unmounted`. Only what goes through `app.mount` and `app.unmount` is reported, not sub-modules.
|
|
39
40
|
- **Options.** `leaks: false` and `modules: false` switch a diagnostic off; `maxListeners` sets the limit; `log` receives the messages instead of the console.
|
|
40
41
|
- **Disconnect.** `bus.use(devPlugin(...))` returns the function that stops the diagnostics and puts `bus.on` and `app.mount` back.
|
|
41
42
|
|
|
@@ -43,6 +44,6 @@ It changes no behavior: it wraps `bus.on`, `app.mount`, and `app.unmount` and ad
|
|
|
43
44
|
|
|
44
45
|
### Keep the import in production
|
|
45
46
|
|
|
46
|
-
`nanoeffects/dev` is a no-op in a production build. `package.json` maps it to an empty build under the `production` export condition (Vite and webpack set that in production mode), so `bus.use(devPlugin(
|
|
47
|
+
`nanoeffects/dev` is a no-op in a production build. `package.json` maps it to an empty build under the `production` export condition (Vite and webpack set that in production mode), so `bus.use(devPlugin())` can stay without a guard: it touches nothing and costs a few bytes.
|
|
47
48
|
|
|
48
49
|
If you want even the call gone, guard it (`if (import.meta.env.DEV)` in Vite). An app that does not import the entry does not carry it.
|
|
@@ -20,7 +20,7 @@ nanoeffects has no store. `createStore` can be replaced by any store you like, f
|
|
|
20
20
|
| event call `loaded(user)` | `bus.emit("loaded", user)` |
|
|
21
21
|
| `createEffect(handler)` | a handler: `bus.on(event, handler)` |
|
|
22
22
|
| effect with `done` and `fail` | [`createJob`](actions.md): emits the success and fail actions |
|
|
23
|
-
| `effect.pending` |
|
|
23
|
+
| `effect.pending` | `job.pending`, `job.success`, `job.fail` and `job.cancel`: set and clear the flag in your store ([Show a loading state](actions.md#show-a-loading-state-with-a-job)) |
|
|
24
24
|
| `sample({ clock, fn, target })` | `bus.on(clock, handler)` and `ctx.emit(target, …)` |
|
|
25
25
|
| `sample({ source: $store, clock })` | read the store from a [context field](context.md) in the handler |
|
|
26
26
|
| `sample({ filter })` | an `if` in the handler |
|
|
@@ -73,16 +73,26 @@ The store (`$items`) is not here. Keep it in Effector and update it from the `fo
|
|
|
73
73
|
|
|
74
74
|
Without Effector, something must hold `$items`. A Zustand store does it: `create` replaces `createStore`, and `setState` replaces `.on(searchFx.doneData, …)`. The handler writes the result to the store directly, so the `found` event is not needed. A component reads the store with a selector, as `useUnit($items)` did.
|
|
75
75
|
|
|
76
|
-
```
|
|
77
|
-
//
|
|
78
|
-
import { create } from "zustand";
|
|
76
|
+
```js
|
|
77
|
+
// bus.js
|
|
79
78
|
import { createBus } from "nanoeffects";
|
|
80
|
-
|
|
79
|
+
|
|
80
|
+
export const bus = createBus(); // the bus: only the bus
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
// search-store.js
|
|
85
|
+
import { create } from "zustand";
|
|
81
86
|
|
|
82
87
|
// create — Zustand: the store replaces $items
|
|
83
|
-
const useSearchStore = create(() => ({ items: [] }));
|
|
88
|
+
export const useSearchStore = create(() => ({ items: [] }));
|
|
89
|
+
```
|
|
84
90
|
|
|
85
|
-
|
|
91
|
+
```js
|
|
92
|
+
// search-handlers.js
|
|
93
|
+
import { debounce, latest } from "nanoeffects/policies";
|
|
94
|
+
import { bus } from "./bus";
|
|
95
|
+
import { useSearchStore } from "./search-store";
|
|
86
96
|
|
|
87
97
|
// latest — a newer query aborts the request in flight; debounce — wait 300 ms of quiet
|
|
88
98
|
const search = latest(async (query, { signal }) => {
|
|
@@ -91,6 +101,12 @@ const search = latest(async (query, { signal }) => {
|
|
|
91
101
|
});
|
|
92
102
|
|
|
93
103
|
bus.on("query-changed", debounce(300, search));
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```jsx
|
|
107
|
+
// Search.jsx
|
|
108
|
+
import { bus } from "./bus";
|
|
109
|
+
import { useSearchStore } from "./search-store";
|
|
94
110
|
|
|
95
111
|
export function Search() {
|
|
96
112
|
const items = useSearchStore((state) => state.items); // Zustand: a selector reads items
|
|
@@ -166,13 +182,18 @@ formSubmitted.watch((event) => {
|
|
|
166
182
|
});
|
|
167
183
|
```
|
|
168
184
|
|
|
169
|
-
The same form with nanoeffects. The values live in React state: nanoeffects holds none, and any store of [Stores](stores.md) works the same way. The effect becomes a handler of the submit event. The values travel in the event, so no `sample` and no `source` are needed. First the bus, in its own file:
|
|
185
|
+
The same form with nanoeffects. The values live in React state: nanoeffects holds none, and any store of [Stores](stores.md) works the same way. The effect becomes a handler of the submit event. The values travel in the event, so no `sample` and no `source` are needed. First the bus and the handler, each in its own file:
|
|
170
186
|
|
|
171
187
|
```js
|
|
172
|
-
//
|
|
188
|
+
// bus.js
|
|
173
189
|
import { createBus } from "nanoeffects";
|
|
174
190
|
|
|
175
|
-
export const bus = createBus(); // the bus:
|
|
191
|
+
export const bus = createBus(); // the bus: only the bus
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
// form-handlers.js
|
|
196
|
+
import { bus } from "./bus";
|
|
176
197
|
|
|
177
198
|
// bus.on — the job of sendFormFx: runs when the form is submitted
|
|
178
199
|
bus.on("form/submitted", (values) => {
|
|
@@ -185,7 +206,8 @@ Then the form. `useState` does the work of `$form`, `fieldUpdate` and `prepend`;
|
|
|
185
206
|
```jsx
|
|
186
207
|
// App.jsx
|
|
187
208
|
import { useState } from "react";
|
|
188
|
-
import { bus } from "./
|
|
209
|
+
import { bus } from "./bus"; // the bus of the previous snippet
|
|
210
|
+
import "./form-handlers"; // registers the handler; the app does this once, when it starts
|
|
189
211
|
|
|
190
212
|
function Field({ name, type, label, value, onChange }) {
|
|
191
213
|
return (
|
|
@@ -255,16 +277,27 @@ Retrying a failed network request, and cancelling when the form closes, are in [
|
|
|
255
277
|
|
|
256
278
|
Keep the Effector stores, and run the effects on the bus. A store calls the bus through `bus.emitter`, and the handler sends the result back as an event that the store listens to.
|
|
257
279
|
|
|
258
|
-
<!-- docs-check: skip - needs effector, which the project does not depend on -->
|
|
259
280
|
```js
|
|
260
|
-
|
|
281
|
+
// bus.js
|
|
261
282
|
import { createBus } from "nanoeffects";
|
|
262
|
-
import {
|
|
263
|
-
import { latest } from "nanoeffects/policies";
|
|
283
|
+
import { emitterPlugin } from "nanoeffects/actions";
|
|
264
284
|
|
|
265
|
-
const bus = createBus().use(emitterPlugin()); // adds bus.emitter
|
|
285
|
+
export const bus = createBus().use(emitterPlugin()); // adds bus.emitter
|
|
286
|
+
```
|
|
266
287
|
|
|
267
|
-
|
|
288
|
+
```js
|
|
289
|
+
// search.js
|
|
290
|
+
import { createAction } from "nanoeffects/actions";
|
|
291
|
+
|
|
292
|
+
export const queryChanged = createAction("query/changed"); // a plain action creator
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
<!-- docs-check: skip - needs effector, which the project does not depend on -->
|
|
296
|
+
```js
|
|
297
|
+
import { createEvent, createStore } from "effector";
|
|
298
|
+
import { latest } from "nanoeffects/policies";
|
|
299
|
+
import { bus } from "./bus";
|
|
300
|
+
import { queryChanged } from "./search";
|
|
268
301
|
|
|
269
302
|
const found = createEvent(); // Effector: the result comes back as an event
|
|
270
303
|
const $items = createStore([]).on(found, (_, items) => items);
|
|
@@ -285,7 +318,7 @@ bus.on(
|
|
|
285
318
|
## What is different
|
|
286
319
|
|
|
287
320
|
- **No state.** There is no `createStore`, `combine`, `restore`, `$store.map`. The bus holds listeners only. Take a store such as Zustand for state ([Stores](stores.md)).
|
|
288
|
-
-
|
|
321
|
+
- **`pending` is an action, not a store.** `createJob` sends `pending` when the handler starts, and `success`, `fail` or `cancel` when it ends; you keep the flag in your store ([Show a loading state](actions.md#show-a-loading-state-with-a-job)). With a policy such as `latest` around the handler the flag is not exact yet, which that section explains. A running task is also visible through [Tracing](tracing.md).
|
|
289
322
|
- **Cancellation is the default tool.** An Effector effect runs every call to the end. Here `latest` and `ctx.signal` stop what is no longer needed.
|
|
290
323
|
- **Events are matched by name, not by identity.** An event is a string or a `createAction` creator, not a unit that you import and call. Two modules agree on the name.
|
|
291
324
|
- **No graph.** `sample` and `forward` build a static graph. A bus subscribes handlers at runtime and the call order is the order of `on`.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Coming from mitt, nanoevents or EventEmitter
|
|
2
|
+
|
|
3
|
+
Read this page if your app uses a small event emitter (`mitt`, `nanoevents`, Node's `EventEmitter`, `tiny-emitter`) and its handlers start to do async work: requests, timers, retries. It maps the calls, shows what you gain, and shows what to check before you switch.
|
|
4
|
+
|
|
5
|
+
## When to switch
|
|
6
|
+
|
|
7
|
+
An emitter calls a function and forgets it. That is enough while handlers are synchronous. Switch when handlers start async work and you hit one of these:
|
|
8
|
+
|
|
9
|
+
- an older request finishes after a newer one and overwrites its result;
|
|
10
|
+
- you keep `AbortController` objects by hand;
|
|
11
|
+
- you write debounce, throttle or retry around handlers yourself;
|
|
12
|
+
- you cannot tell which event started which request.
|
|
13
|
+
|
|
14
|
+
If handlers stay synchronous, keep your emitter. nanoeffects costs about 1.4 kB gzip (1.3 kB brotli), close to the small emitters.
|
|
15
|
+
|
|
16
|
+
## Find the equivalent
|
|
17
|
+
|
|
18
|
+
| mitt / EventEmitter | nanoeffects |
|
|
19
|
+
| ------------------- | ----------- |
|
|
20
|
+
| `emitter.on("a", handler)` | `bus.on("a", handler)` |
|
|
21
|
+
| `emitter.off("a", handler)` | call the function that `bus.on` returned, or `bus.off("a")` for all handlers of `a` |
|
|
22
|
+
| `emitter.emit("a", payload)` | `bus.emit("a", payload)` |
|
|
23
|
+
| `emitter.once("a", handler)` | `bus.once("a", handler)` |
|
|
24
|
+
| `on("*", (type, payload) => …)` | `bus.on("*", (payload, ctx) => …)`: the event name is `ctx.type` (see the [Guide](guide.md)) |
|
|
25
|
+
| `emitter.all.clear()` | `bus.off()` |
|
|
26
|
+
|
|
27
|
+
Two differences to know:
|
|
28
|
+
|
|
29
|
+
- `bus.on` returns the unsubscribe function. You do not need to keep the handler to remove it.
|
|
30
|
+
- The handler gets a second argument, the task context: `(payload, ctx)`. You can ignore it until you need `ctx.signal` or `ctx.emit`.
|
|
31
|
+
|
|
32
|
+
## Move a handler
|
|
33
|
+
|
|
34
|
+
Before, with `mitt`:
|
|
35
|
+
|
|
36
|
+
<!-- docs-check: skip - needs mitt, which the project does not depend on -->
|
|
37
|
+
```js
|
|
38
|
+
import mitt from "mitt";
|
|
39
|
+
|
|
40
|
+
const emitter = mitt();
|
|
41
|
+
|
|
42
|
+
emitter.on("search", async (query) => {
|
|
43
|
+
const items = await api.search(query);
|
|
44
|
+
emitter.emit("results", items);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
emitter.emit("search", "abc");
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
After, with nanoeffects. The first step changes only the import and the constructor:
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
import { createBus } from "nanoeffects";
|
|
54
|
+
|
|
55
|
+
const bus = createBus(); // event bus; each handler call is a task
|
|
56
|
+
|
|
57
|
+
bus.on("search", async (query) => {
|
|
58
|
+
const items = await api.search(query);
|
|
59
|
+
bus.emit("results", items);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
bus.emit("search", "abc");
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The second step fixes the race. Wrap the handler in `latest` and pass the signal to the request:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
import { createBus } from "nanoeffects";
|
|
69
|
+
import { latest } from "nanoeffects/policies";
|
|
70
|
+
|
|
71
|
+
const bus = createBus(); // event bus; each handler call is a task
|
|
72
|
+
|
|
73
|
+
// latest: a newer "search" aborts the older run
|
|
74
|
+
bus.on(
|
|
75
|
+
"search",
|
|
76
|
+
latest(async (query, { signal }) => {
|
|
77
|
+
const items = await api.search(query, { signal }); // signal cancels the request
|
|
78
|
+
bus.emit("results", items);
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
bus.emit("search", "abc");
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
More cases (pause, retry, timeout): [Cancel stale requests](cancel-stale-requests.md).
|
|
86
|
+
|
|
87
|
+
## Check the differences
|
|
88
|
+
|
|
89
|
+
| Topic | Emitter | nanoeffects |
|
|
90
|
+
| ----- | ------- | ----------- |
|
|
91
|
+
| Handler order | order of subscription | order of subscription |
|
|
92
|
+
| Error in a handler | depends on the library | the first error stops the rest and is thrown from `emit`; set `onError` to isolate handlers ([Errors](errors.md)) |
|
|
93
|
+
| Async handler rejects | usually an unhandled rejection | not handled without `onError`; set `onError` to catch it |
|
|
94
|
+
| Event without a listener | ignored | ignored; set `onUnhandled` to be told |
|
|
95
|
+
| Types | event map in a type parameter | event map in a type parameter ([TypeScript](typescript.md)) |
|
|
96
|
+
|
|
97
|
+
The exact rules of `emit`, abort and ordering: [Semantics](semantics.md).
|
|
@@ -90,16 +90,27 @@ Keep RxJS where a stream is how you think, and let events cross to the bus in ei
|
|
|
90
90
|
|
|
91
91
|
**RxJS to bus.** `bus.emitter(action)` returns a callback that emits the action, so it is the argument of `subscribe` ([Stores](stores.md) has the plugin).
|
|
92
92
|
|
|
93
|
-
<!-- docs-check: skip - needs rxjs, which the project does not depend on -->
|
|
94
93
|
```js
|
|
95
|
-
|
|
94
|
+
// bus.js
|
|
96
95
|
import { createBus } from "nanoeffects";
|
|
97
|
-
import {
|
|
98
|
-
|
|
96
|
+
import { emitterPlugin } from "nanoeffects/actions";
|
|
97
|
+
|
|
98
|
+
export const bus = createBus().use(emitterPlugin()); // adds bus.emitter
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
// filters.js
|
|
103
|
+
import { createAction } from "nanoeffects/actions";
|
|
99
104
|
|
|
100
|
-
const
|
|
105
|
+
export const filtersChanged = createAction("filters/changed"); // a plain action creator
|
|
106
|
+
```
|
|
101
107
|
|
|
102
|
-
|
|
108
|
+
<!-- docs-check: skip - needs rxjs, which the project does not depend on -->
|
|
109
|
+
```js
|
|
110
|
+
import { combineLatest } from "rxjs";
|
|
111
|
+
import { latest } from "nanoeffects/policies";
|
|
112
|
+
import { bus } from "./bus";
|
|
113
|
+
import { filtersChanged } from "./filters";
|
|
103
114
|
|
|
104
115
|
// RxJS: combine two streams into one value; nanoeffects does not do that
|
|
105
116
|
const subscription = combineLatest([query$, category$]).subscribe(
|