nanoeffects 0.6.0 → 0.8.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/dist/{action-creator-c292nK7v.d.ts → action-creator-BrZF6hXc.d.ts} +4 -8
- package/dist/actions.d.ts +6 -6
- package/dist/actions.js +1 -1
- package/dist/{context-plugin-1pYmSQEG.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/match.d.ts +50 -0
- package/dist/match.js +1 -0
- package/dist/{module-BlF2KSgy.d.ts → module-DqKegXHt.d.ts} +44 -19
- package/dist/notrace/{action-creator-DKG_sIia.d.ts → action-creator-DHAEgwuh.d.ts} +4 -8
- package/dist/notrace/actions.d.ts +59 -24
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/{context-plugin-Dt1IgVo8.d.ts → context-plugin-BapQkGwQ.d.ts} +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/{plugin-B0AXKusc.d.ts → plugin-Cxadz6uU.d.ts} +2 -2
- package/dist/notrace/policies.d.ts +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-CCYlC0XS.d.ts → plugin-DkXCvsmB.d.ts} +2 -2
- package/dist/policies.d.ts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/redux.d.ts +1 -1
- package/dist/scope.d.ts +8 -9
- package/dist/scope.js +1 -1
- package/dist/trace.d.ts +1 -1
- package/dist/types-KavbvHix.d.ts +25 -0
- package/docs/README.md +2 -0
- package/docs/actions.md +80 -36
- package/docs/guide.md +142 -18
- package/docs/hmr.md +6 -5
- package/docs/migrating-from-effector.md +49 -16
- package/docs/migrating-from-rxjs.md +17 -6
- package/docs/oop.md +286 -0
- package/docs/package.md +4 -0
- package/docs/plugins.md +284 -0
- package/docs/react.md +17 -5
- package/docs/scope.md +76 -36
- package/docs/stores.md +263 -109
- package/llms-full.txt +1232 -231
- package/llms.txt +2 -0
- package/package.json +20 -2
- package/dist/types-mLaAx-EZ.d.ts +0 -15
package/llms-full.txt
CHANGED
|
@@ -86,6 +86,74 @@ unbind();
|
|
|
86
86
|
- An event that only `"*"` listens to counts as heard, so `onUnhandled` does not fire for it.
|
|
87
87
|
- `bus.off("*")` removes every `"*"` listener; `bus.off()` clears the whole bus, `"*"` included.
|
|
88
88
|
|
|
89
|
+
### Pick events with a function
|
|
90
|
+
|
|
91
|
+
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.
|
|
92
|
+
|
|
93
|
+
<!-- docs-check: new -->
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
// bus.js
|
|
97
|
+
import { createBus } from "nanoeffects";
|
|
98
|
+
import { matchPlugin } from "nanoeffects/match";
|
|
99
|
+
|
|
100
|
+
// matchPlugin — lets on, once and off take a function that picks the events
|
|
101
|
+
export const bus = createBus().use(matchPlugin());
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
// user.js
|
|
106
|
+
import { createAction } from "nanoeffects/actions";
|
|
107
|
+
|
|
108
|
+
export const userLoaded = createAction("user/loaded");
|
|
109
|
+
export const userFailed = createAction("user/failed");
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
// user-handlers.js
|
|
114
|
+
import { bus } from "./bus";
|
|
115
|
+
|
|
116
|
+
// the function picks the events; the handler gets the payload and the context of each
|
|
117
|
+
bus.on(
|
|
118
|
+
(action) => action.type.startsWith("user/"),
|
|
119
|
+
(payload, ctx) => {
|
|
120
|
+
console.log(ctx.type, payload);
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
import { bus } from "./bus";
|
|
127
|
+
import { userFailed, userLoaded } from "./user";
|
|
128
|
+
|
|
129
|
+
bus.emit(userLoaded({ id: "1" })); // user/loaded { id: "1" }
|
|
130
|
+
bus.emit("order/placed", 5); // not picked
|
|
131
|
+
bus.emit(userFailed("boom")); // user/failed boom
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- `once(fn, handler)` waits for the first event that `fn` accepts. `off(fn)` removes every handler of `fn`, and `off(fn, handler)` removes one.
|
|
135
|
+
- An action creator or a command still means that one event: only a function with no `type` is a function that picks events.
|
|
136
|
+
- 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.
|
|
137
|
+
- It works in any order with the other plugins, `devPlugin` included.
|
|
138
|
+
|
|
139
|
+
In TypeScript a function that is a type guard narrows the payload of the handler:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { bus } from "./bus";
|
|
143
|
+
|
|
144
|
+
type User = { id: string };
|
|
145
|
+
|
|
146
|
+
// the guard says which event this is, and the handler gets its payload typed
|
|
147
|
+
const isLoaded = (action: {
|
|
148
|
+
type: string;
|
|
149
|
+
payload: unknown;
|
|
150
|
+
}): action is { type: "user/loaded"; payload: User } => action.type === "user/loaded";
|
|
151
|
+
|
|
152
|
+
bus.on(isLoaded, (user) => console.log(user.id)); // user: User
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`ctx.on` takes such a function at run time too, but its type does not know it yet.
|
|
156
|
+
|
|
89
157
|
## 2. Use the handler context
|
|
90
158
|
|
|
91
159
|
The second argument is the **task** context for this call. Emit further events through it. Pass `ctx.signal` to `fetch` when you cancel work.
|
|
@@ -242,22 +310,42 @@ More in [Effects](effects.md).
|
|
|
242
310
|
|
|
243
311
|
`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).
|
|
244
312
|
|
|
313
|
+
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.
|
|
314
|
+
|
|
245
315
|
```js
|
|
316
|
+
// bus.js
|
|
246
317
|
import { createBus } from "nanoeffects";
|
|
247
|
-
import { createAction, createCommand } from "nanoeffects/actions";
|
|
248
318
|
|
|
249
|
-
const bus = createBus();
|
|
319
|
+
export const bus = createBus();
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
```js
|
|
323
|
+
// move.js
|
|
324
|
+
import { createAction } from "nanoeffects/actions";
|
|
325
|
+
|
|
326
|
+
// createAction — names an event; calling it builds { type, payload }
|
|
327
|
+
export const moved = createAction("moved");
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
```js
|
|
331
|
+
// math.js
|
|
332
|
+
import { createCommand } from "nanoeffects/actions";
|
|
333
|
+
|
|
334
|
+
// createCommand — like an action, but emit runs a handler and returns its result
|
|
335
|
+
export const double = createCommand("math/double", (value) => value * 2);
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
```js
|
|
339
|
+
import { bus } from "./bus";
|
|
340
|
+
import { moved } from "./move";
|
|
341
|
+
import { double } from "./math";
|
|
250
342
|
|
|
251
343
|
bus.emit("moved", { x: 1, y: 2 });
|
|
252
344
|
bus.emit({ type: "moved", payload: { x: 2, y: 2 } });
|
|
253
345
|
|
|
254
|
-
// createAction — names an event; calling it builds { type, payload }
|
|
255
|
-
const moved = createAction("moved");
|
|
256
346
|
bus.emit(moved({ x: 3, y: 2 }));
|
|
257
347
|
bus.on(moved, ({ x }) => console.log(x));
|
|
258
348
|
|
|
259
|
-
// createCommand — like an action, but emit runs a handler and returns its result
|
|
260
|
-
const double = createCommand("math/double", (value) => value * 2);
|
|
261
349
|
bus.emit(double(21)); // 42
|
|
262
350
|
```
|
|
263
351
|
|
|
@@ -268,14 +356,25 @@ Use strings for the plain case. Use objects when other code already builds actio
|
|
|
268
356
|
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.
|
|
269
357
|
|
|
270
358
|
```js
|
|
271
|
-
|
|
272
|
-
import { createCommand
|
|
359
|
+
// user.js
|
|
360
|
+
import { createCommand } from "nanoeffects/actions";
|
|
273
361
|
|
|
274
|
-
const getUser = createCommand("user/get"); // contract — no handler yet
|
|
362
|
+
export const getUser = createCommand("user/get"); // contract — no handler yet
|
|
363
|
+
```
|
|
275
364
|
|
|
276
|
-
|
|
365
|
+
```js
|
|
366
|
+
// bus.js
|
|
367
|
+
import { createBus } from "nanoeffects";
|
|
368
|
+
import { commandsPlugin } from "nanoeffects/actions";
|
|
369
|
+
|
|
370
|
+
export const bus = createBus();
|
|
277
371
|
// commandsPlugin — registry of handlers for this bus
|
|
278
|
-
const commands = bus.use(commandsPlugin());
|
|
372
|
+
export const commands = bus.use(commandsPlugin());
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
```js
|
|
376
|
+
import { bus, commands } from "./bus";
|
|
377
|
+
import { getUser } from "./user";
|
|
279
378
|
|
|
280
379
|
bus.emit(getUser("1")); // throws: No handler for command "user/get"
|
|
281
380
|
|
|
@@ -296,16 +395,27 @@ Each bus has its own registry. A [module](#9-mount-a-domain-as-a-module) can reg
|
|
|
296
395
|
**What you do.** `createModule(name, setup)` describes the feature. `bus.use(modulePlugin([…]))` mounts it. `app.unmount(module)` undoes everything the setup registered.
|
|
297
396
|
|
|
298
397
|
```js
|
|
299
|
-
|
|
300
|
-
import { createCommand
|
|
398
|
+
// user.js
|
|
399
|
+
import { createCommand } from "nanoeffects/actions";
|
|
301
400
|
|
|
302
|
-
const getUser = createCommand("user/get"); // contract — no handler yet
|
|
303
|
-
|
|
401
|
+
export const getUser = createCommand("user/get"); // contract — no handler yet
|
|
402
|
+
```
|
|
304
403
|
|
|
305
|
-
|
|
404
|
+
```js
|
|
405
|
+
// math.js
|
|
406
|
+
import { createCommand } from "nanoeffects/actions";
|
|
407
|
+
|
|
408
|
+
export const double = createCommand("math/double", (value) => value * 2); // carries its handler
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
```js
|
|
412
|
+
// greeting-module.js
|
|
413
|
+
import { createModule } from "nanoeffects/actions";
|
|
414
|
+
import { getUser } from "./user";
|
|
415
|
+
import { double } from "./math";
|
|
306
416
|
|
|
307
417
|
// createModule — one feature; name is stable for hot reload replace
|
|
308
|
-
const greeting = createModule("greeting", (m) => {
|
|
418
|
+
export const greeting = createModule("greeting", (m) => {
|
|
309
419
|
// m.on — like bus.on, removed automatically on unmount
|
|
310
420
|
m.on("greet", (name) => console.log(`hello ${name}`));
|
|
311
421
|
|
|
@@ -315,9 +425,23 @@ const greeting = createModule("greeting", (m) => {
|
|
|
315
425
|
// m.use — register a command that already has a handler, for this module’s life
|
|
316
426
|
m.use(double);
|
|
317
427
|
});
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
```js
|
|
431
|
+
// bus.js
|
|
432
|
+
import { createBus } from "nanoeffects";
|
|
433
|
+
import { modulePlugin } from "nanoeffects/actions";
|
|
434
|
+
import { greeting } from "./greeting-module";
|
|
318
435
|
|
|
436
|
+
export const bus = createBus();
|
|
319
437
|
// modulePlugin — mount modules on the bus
|
|
320
|
-
const app = bus.use(modulePlugin([greeting]));
|
|
438
|
+
export const app = bus.use(modulePlugin([greeting]));
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
```js
|
|
442
|
+
import { app, bus } from "./bus";
|
|
443
|
+
import { greeting } from "./greeting-module";
|
|
444
|
+
|
|
321
445
|
bus.emit("greet", "Ada"); // hello Ada
|
|
322
446
|
app.unmount(greeting); // greet listener and getUser binding are gone
|
|
323
447
|
```
|
|
@@ -389,6 +513,8 @@ Imports are split so you only load what you use. `createBus` stays small; the re
|
|
|
389
513
|
| `nanoeffects/policies` | `latest`, `leading`, `debounce`, `throttle`, `queue` |
|
|
390
514
|
| `nanoeffects/effects` | `retry`, `timeout`, `poll`, `delay`, `take`, `all`, `allSettled`, `race`, `effectsPlugin` |
|
|
391
515
|
| `nanoeffects/actions` | `createAction`, `createCommand`, `createJob`, `createModule`, `modulePlugin`, `commandsPlugin` |
|
|
516
|
+
| `nanoeffects/match` | `matchPlugin`: `on`, `once` and `off` that take a function which picks the events |
|
|
517
|
+
| `nanoeffects/oop` | `extendBus`, `WithBus`, `On`, `Once`, `disposeBus`: the methods of a bus on a class, with the subscriptions of each instance ended by `dispose()` |
|
|
392
518
|
| `nanoeffects/scope` | `scopePlugin`, `effect`: fields for one command, action, job, module or `emit` (the types `Scope`, `Effect`) |
|
|
393
519
|
| `nanoeffects/redux` | `createEffectsMiddleware` |
|
|
394
520
|
| `nanoeffects/observable` | `createObserve` |
|
|
@@ -396,6 +522,8 @@ Imports are split so you only load what you use. `createBus` stays small; the re
|
|
|
396
522
|
| `nanoeffects/dev` | `devPlugin` (no-op in production) |
|
|
397
523
|
| `nanoeffects/trace` | `createTracer` and the loggers |
|
|
398
524
|
|
|
525
|
+
The `...Plugin` names above are used with `bus.use`. [Plugins](plugins.md) lists all of them, what each returns, and the order to use them in.
|
|
526
|
+
|
|
399
527
|
## Give the docs to an AI assistant
|
|
400
528
|
|
|
401
529
|
The package ships two plain-text files for assistants that write code with it:
|
|
@@ -405,6 +533,294 @@ The package ships two plain-text files for assistants that write code with it:
|
|
|
405
533
|
|
|
406
534
|
Find them in `node_modules/nanoeffects/`, or load `llms-full.txt` from a CDN such as `https://unpkg.com/nanoeffects/llms-full.txt`. Both match the installed version.
|
|
407
535
|
|
|
536
|
+
---
|
|
537
|
+
Source: docs/plugins.md
|
|
538
|
+
|
|
539
|
+
# Choose the plugins of a bus
|
|
540
|
+
|
|
541
|
+
Optional. A plugin adds one thing to a bus: fields on `ctx`, a method, a registry of handlers, diagnostics. Read this page when you want to know which plugins exist, what each one gives you, and in what order to use them. Skip it for a small bus: `createBus()` alone is enough.
|
|
542
|
+
|
|
543
|
+
## Use a plugin
|
|
544
|
+
|
|
545
|
+
A plugin is a function of the bus. `bus.use(plugin)` calls it with the bus and returns whatever the plugin returns. Most plugins return the same bus, so you can chain them: `createBus().use(a).use(b)`. A few return something of their own (a registry, an app, a `disconnect` function): keep it.
|
|
546
|
+
|
|
547
|
+
In TypeScript keep the result of `contextPlugin` and `effectsPlugin`: the type of the bus with its new fields is the type they return (`const bus = createBus().use(contextPlugin({ store }))`).
|
|
548
|
+
|
|
549
|
+
## Main plugins
|
|
550
|
+
|
|
551
|
+
These four cover what most apps need: shared dependencies, effects, commands and modules. Start here.
|
|
552
|
+
|
|
553
|
+
| Plugin | What it gives you | Import | `bus.use` returns |
|
|
554
|
+
| ------ | ----------------- | ------ | ----------------- |
|
|
555
|
+
| [`contextPlugin`](#put-a-store-or-a-client-on-every-handler-contextplugin) | Your own fields (a store, a client, a logger) on `ctx` of every handler. | `nanoeffects` | The same bus, typed with the fields |
|
|
556
|
+
| [`effectsPlugin`](#use-race-and-delay-without-ctx-effectsplugin) | `race`, `all`, `delay` and your own effects on `ctx`, called without passing `ctx`. | `nanoeffects/effects` | The same bus, typed with the effects |
|
|
557
|
+
| [`commandsPlugin`](#let-a-test-replace-a-handler-commandsplugin) | The handlers of commands in a registry, so a test can replace one. | `nanoeffects/actions` | The registry `{ run, handle }` |
|
|
558
|
+
| [`modulePlugin`](#mount-a-domain-as-one-unit-moduleplugin) | A module: the listeners, handlers and tasks of one domain, mounted and unmounted with one call. | `nanoeffects/actions` | The app `{ mount, unmount, isMounted, dispose, commands, createModule }` |
|
|
559
|
+
|
|
560
|
+
### Put a store or a client on every handler: `contextPlugin`
|
|
561
|
+
|
|
562
|
+
Every task gets the same fields on `ctx`: a store, an API client, a logger. The types come from the values. More in [Context fields](context.md).
|
|
563
|
+
|
|
564
|
+
```js
|
|
565
|
+
import { contextPlugin, createBus } from "nanoeffects";
|
|
566
|
+
|
|
567
|
+
// contextPlugin — every handler gets ctx.store
|
|
568
|
+
const bus = createBus().use(contextPlugin({ store }));
|
|
569
|
+
|
|
570
|
+
bus.on("save", (draft, { store }) => store.put(draft));
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### Use `race` and `delay` without `ctx`: `effectsPlugin`
|
|
574
|
+
|
|
575
|
+
Effects such as `race`, `all`, `take` and `delay` are bound to the task, so a handler calls `race({ … })` and not `race(ctx, { … })`. Your own functions that take `{ signal }` first work too. More in [Effects](effects.md#put-selected-effects-on-ctx).
|
|
576
|
+
|
|
577
|
+
```js
|
|
578
|
+
import { createBus } from "nanoeffects";
|
|
579
|
+
import { delay, effectsPlugin, race } from "nanoeffects/effects";
|
|
580
|
+
|
|
581
|
+
// effectsPlugin — race and delay are bound to the task of each handler
|
|
582
|
+
const bus = createBus().use(effectsPlugin({ race, delay }));
|
|
583
|
+
|
|
584
|
+
bus.on("load", async (id, { race, delay }) => {
|
|
585
|
+
const result = await race({
|
|
586
|
+
data: ({ signal }) => api.get(id, { signal }),
|
|
587
|
+
timeout: () => delay(5000),
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
### Let a test replace a handler: `commandsPlugin`
|
|
593
|
+
|
|
594
|
+
Commands get their handlers from a registry that belongs to the bus. A command can be only a contract, and a test binds a fake. More in [Actions](actions.md#describe-events-commands-and-jobs).
|
|
595
|
+
|
|
596
|
+
```js
|
|
597
|
+
import { createBus } from "nanoeffects";
|
|
598
|
+
import { commandsPlugin, createCommand } from "nanoeffects/actions";
|
|
599
|
+
|
|
600
|
+
const getUser = createCommand("user/get"); // a contract: no handler yet
|
|
601
|
+
const bus = createBus();
|
|
602
|
+
|
|
603
|
+
// commandsPlugin — the registry of handlers of this bus
|
|
604
|
+
const commands = bus.use(commandsPlugin());
|
|
605
|
+
|
|
606
|
+
commands.handle(getUser, async (id) => ({ id })); // the latest binding wins
|
|
607
|
+
const user = await bus.emit(getUser("1")); // { id: "1" }
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### Mount a domain as one unit: `modulePlugin`
|
|
611
|
+
|
|
612
|
+
A module groups the listeners, the bound commands and the background tasks of one domain, and ends all of them when it is unmounted. It also makes [hot reload](hmr.md) work. More in [Modules](actions.md#mount-domains-with-modules).
|
|
613
|
+
|
|
614
|
+
```js
|
|
615
|
+
import { createBus } from "nanoeffects";
|
|
616
|
+
import { modulePlugin } from "nanoeffects/actions";
|
|
617
|
+
|
|
618
|
+
const bus = createBus();
|
|
619
|
+
|
|
620
|
+
// modulePlugin — mounts userModule on the first use of the app or the bus, and returns the app
|
|
621
|
+
const app = bus.use(modulePlugin([userModule]));
|
|
622
|
+
|
|
623
|
+
app.unmount(userModule); // its listeners, bound commands and tasks are gone
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
## Extra plugins
|
|
627
|
+
|
|
628
|
+
Add one of these when you meet its need. None of them is required.
|
|
629
|
+
|
|
630
|
+
| Plugin | What it gives you | Import | `bus.use` returns |
|
|
631
|
+
| ------ | ----------------- | ------ | ----------------- |
|
|
632
|
+
| [`scopePlugin`](#give-one-command-its-own-fields-scopeplugin) | Its own fields for one command, action, job or module, and a way to replace them in one `emit`. | `nanoeffects/scope` | The same bus |
|
|
633
|
+
| [`matchPlugin`](#listen-to-a-group-of-events-matchplugin) | `on` that listens to a group of events which a function picks. | `nanoeffects/match` | The same bus, typed with the new overloads |
|
|
634
|
+
| [`emitterPlugin`](#hand-the-bus-to-a-state-manager-emitterplugin) | A callback that emits an action, to give to a state manager. | `nanoeffects/actions` | The same bus, with `emitter` |
|
|
635
|
+
| [`reactPlugin`](#tell-the-bus-when-a-react-value-changes-reactplugin) | A React hook that emits when a value of a component changes. | `nanoeffects/react` | The same bus, with `useEmitter` |
|
|
636
|
+
| [`devPlugin`](#find-leaks-while-you-develop-devplugin) | Warnings about listeners that pile up, and messages about modules, while you develop. | `nanoeffects/dev` | A function that disconnects it |
|
|
637
|
+
|
|
638
|
+
### Give one command its own fields: `scopePlugin`
|
|
639
|
+
|
|
640
|
+
A command, an action, a job or a module can carry its own fields and effects, and one `emit` can replace them. A test gives a fake to one call. More in [Scopes](scope.md).
|
|
641
|
+
|
|
642
|
+
```js
|
|
643
|
+
import { createBus } from "nanoeffects";
|
|
644
|
+
import { createCommand } from "nanoeffects/actions";
|
|
645
|
+
import { scopePlugin } from "nanoeffects/scope";
|
|
646
|
+
|
|
647
|
+
// scopePlugin — makes the scope of a command work on this bus
|
|
648
|
+
const bus = createBus().use(scopePlugin());
|
|
649
|
+
|
|
650
|
+
// the last argument: fields of this command only
|
|
651
|
+
const getUser = createCommand("user/get", (id, { api }) => api.getUser(id), {
|
|
652
|
+
api,
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
const fakeApi = { getUser: async (id) => ({ id, name: "test" }) };
|
|
656
|
+
bus.emit(getUser("1"), { api: fakeApi }); // a fake api for this call only
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### Listen to a group of events: `matchPlugin`
|
|
660
|
+
|
|
661
|
+
`on`, `once` and `off` take a function that picks the events, such as every `user/*` event. A type guard narrows the payload. The subscription listens to every event, so for a few known events `on(creator, handler)` is cheaper. More in the [Guide](guide.md#pick-events-with-a-function).
|
|
662
|
+
|
|
663
|
+
```js
|
|
664
|
+
import { createBus } from "nanoeffects";
|
|
665
|
+
import { matchPlugin } from "nanoeffects/match";
|
|
666
|
+
|
|
667
|
+
// matchPlugin — on takes a function that picks the events
|
|
668
|
+
const bus = createBus().use(matchPlugin());
|
|
669
|
+
|
|
670
|
+
bus.on(
|
|
671
|
+
(action) => action.type.startsWith("user/"),
|
|
672
|
+
(payload, ctx) => console.log(ctx.type, payload),
|
|
673
|
+
);
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### Hand the bus to a state manager: `emitterPlugin`
|
|
677
|
+
|
|
678
|
+
`bus.emitter(creator)` is a callback that emits an action. A state manager takes it as its subscription callback, so a change of the store becomes an event. More in [Stores](stores.md#emit-from-a-state-manager).
|
|
679
|
+
|
|
680
|
+
```js
|
|
681
|
+
import { atom } from "nanostores";
|
|
682
|
+
import { createBus } from "nanoeffects";
|
|
683
|
+
import { createAction, emitterPlugin } from "nanoeffects/actions";
|
|
684
|
+
|
|
685
|
+
// emitterPlugin — adds bus.emitter
|
|
686
|
+
const bus = createBus().use(emitterPlugin());
|
|
687
|
+
const nameChanged = createAction("name/changed");
|
|
688
|
+
const $name = atom("");
|
|
689
|
+
|
|
690
|
+
$name.listen(bus.emitter(nameChanged)); // every change of $name emits nameChanged
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
### Tell the bus when a React value changes: `reactPlugin`
|
|
694
|
+
|
|
695
|
+
`useEmitter(event, value)` emits after a render commits with a new value. It does not emit on mount, and not twice under StrictMode. It needs React 18 or newer. More in [Stores](stores.md#react-sharing-a-value-across-components).
|
|
696
|
+
|
|
697
|
+
```jsx
|
|
698
|
+
import { useState } from "react";
|
|
699
|
+
import { createBus } from "nanoeffects";
|
|
700
|
+
import { reactPlugin } from "nanoeffects/react";
|
|
701
|
+
|
|
702
|
+
// reactPlugin — adds bus.useEmitter; destructuring gives a free hook
|
|
703
|
+
const bus = createBus().use(reactPlugin());
|
|
704
|
+
const { useEmitter } = bus;
|
|
705
|
+
|
|
706
|
+
function Profile() {
|
|
707
|
+
const [name, setName] = useState("");
|
|
708
|
+
useEmitter("name/changed", name); // emits after a render with a new name
|
|
709
|
+
return <input value={name} onChange={(event) => setName(event.target.value)} />;
|
|
710
|
+
}
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
### Find leaks while you develop: `devPlugin`
|
|
714
|
+
|
|
715
|
+
It warns about listeners that were added and never unbound, and reports the modules that are mounted, replaced and unmounted. In a production build `nanoeffects/dev` is empty, so the call can stay. More in [Hot reload](hmr.md#report-leaks-with-devplugin).
|
|
716
|
+
|
|
717
|
+
```js
|
|
718
|
+
import { createBus } from "nanoeffects";
|
|
719
|
+
import { modulePlugin } from "nanoeffects/actions";
|
|
720
|
+
import { devPlugin } from "nanoeffects/dev";
|
|
721
|
+
|
|
722
|
+
const bus = createBus();
|
|
723
|
+
bus.use(modulePlugin([userModule]));
|
|
724
|
+
|
|
725
|
+
// devPlugin — warnings and messages in the console while you develop;
|
|
726
|
+
// it finds the app of modulePlugin on the bus, whichever you use first
|
|
727
|
+
const disconnect = bus.use(devPlugin());
|
|
728
|
+
|
|
729
|
+
disconnect(); // stops the diagnostics
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
## Put them together
|
|
733
|
+
|
|
734
|
+
One bus with every plugin that does not need React, in a file of its own. The events of the app stay in other files. The plugins work in any order; the next section says what that means for modules.
|
|
735
|
+
|
|
736
|
+
```js
|
|
737
|
+
// bus.js
|
|
738
|
+
import { contextPlugin, createBus } from "nanoeffects";
|
|
739
|
+
import {
|
|
740
|
+
commandsPlugin,
|
|
741
|
+
emitterPlugin,
|
|
742
|
+
modulePlugin,
|
|
743
|
+
} from "nanoeffects/actions";
|
|
744
|
+
import { devPlugin } from "nanoeffects/dev";
|
|
745
|
+
import { all, effectsPlugin, race } from "nanoeffects/effects";
|
|
746
|
+
import { matchPlugin } from "nanoeffects/match";
|
|
747
|
+
import { scopePlugin } from "nanoeffects/scope";
|
|
748
|
+
|
|
749
|
+
export const bus = createBus()
|
|
750
|
+
.use(contextPlugin({ store })) // ctx.store on every task
|
|
751
|
+
.use(effectsPlugin({ race, all })) // ctx.race and ctx.all, bound to the task
|
|
752
|
+
.use(scopePlugin()) // scopes for commands, actions, jobs, modules and emit
|
|
753
|
+
.use(matchPlugin()) // bus.on takes a function that picks the events
|
|
754
|
+
.use(emitterPlugin()); // bus.emitter(creator)
|
|
755
|
+
|
|
756
|
+
// commandsPlugin — the registry of command handlers; keep it to bind handlers later
|
|
757
|
+
export const commands = bus.use(commandsPlugin());
|
|
758
|
+
|
|
759
|
+
// modulePlugin — mounts userModule on the first use; keep app to mount and unmount later
|
|
760
|
+
export const app = bus.use(modulePlugin([userModule]));
|
|
761
|
+
|
|
762
|
+
// devPlugin — reports leaks and modules while you develop; keep disconnect to remove it
|
|
763
|
+
export const disconnect = bus.use(devPlugin());
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
## Use them in any order
|
|
767
|
+
|
|
768
|
+
The plugins give the same result in any order. Use them as they read best, and put the app of `modulePlugin` where you keep it.
|
|
769
|
+
|
|
770
|
+
Two things happen because modules are mounted late:
|
|
771
|
+
|
|
772
|
+
- **Modules start on the first use.** `modulePlugin([userModule])` mounts `userModule` when the app or the bus is first used: the first `bus.emit`, or the first call of `app.mount`, `app.unmount`, `app.isMounted`, `app.dispose` or the read of `app.commands`. By then every plugin is on the bus, so a module and the tasks it spawns have the fields, effects and scopes of the plugins you used after `modulePlugin`, and `m.on(fn, handler)` works with `matchPlugin`.
|
|
773
|
+
- **Listeners keep their place.** A `bus.on` after `bus.use(modulePlugin(...))` is held until the modules are mounted, then made, in the order you wrote them. The listeners of a module therefore stay ahead of the ones registered after the plugin, and behind the ones registered before it. The function `bus.on` returns works before and after.
|
|
774
|
+
|
|
775
|
+
Other plugins do not need an order for another reason:
|
|
776
|
+
|
|
777
|
+
- `commandsPlugin` binds its list in the registry that the bus has, so it can come before or after `modulePlugin`. A custom `bus.commands = { run }` is replaced.
|
|
778
|
+
- `effectsPlugin` and `contextPlugin` do not replace a field that is on `ctx` already, so a scope wins in any order.
|
|
779
|
+
- `devPlugin()` finds the app that `modulePlugin` put on the bus (`bus.app`). `devPlugin({ app })` still works.
|
|
780
|
+
- `matchPlugin` and `devPlugin` both wrap `bus.on`; `devPlugin` counts a function that picks events under `"*"`, and its `disconnect` leaves a wrapper that was put on top of it.
|
|
781
|
+
|
|
782
|
+
A name that is already a context field of the bus is a type error when `contextPlugin` or `effectsPlugin` adds it again.
|
|
783
|
+
|
|
784
|
+
A test puts every plugin on a bus in many orders and checks what each one does (`src/tests/plugin-matrix.test.ts`). An order that breaks something is a bug.
|
|
785
|
+
|
|
786
|
+
## See what is not a plugin
|
|
787
|
+
|
|
788
|
+
These connect the bus to something, but are not used with `bus.use`:
|
|
789
|
+
|
|
790
|
+
- `createEffectsMiddleware(bus, { filter })` from `nanoeffects/redux` is a Redux middleware for a bus that exists. See [Stores](stores.md#wire-redux-toolkit-to-a-bus).
|
|
791
|
+
- `createBus({ trace })` takes a tracer from `nanoeffects/trace`. See [Tracing](tracing.md).
|
|
792
|
+
- `extendBus(bus, Base)` from `nanoeffects/oop` gives a class the methods of a bus, with the subscriptions of each instance ended by `dispose()`. It reads the bus as it is, so use the plugins first. See [Use the bus from a class](oop.md).
|
|
793
|
+
- `createObserve(bus)` from `nanoeffects/observable` turns an event into an Observable. See [Observable](observable.md).
|
|
794
|
+
|
|
795
|
+
## Write your own plugin
|
|
796
|
+
|
|
797
|
+
A plugin is a function of the bus that returns what you want back. The bus keeps no list of plugins, so a plugin that was used cannot be taken out by the bus; it returns its own `stop` or `disconnect` when it needs one.
|
|
798
|
+
|
|
799
|
+
```js
|
|
800
|
+
// count-plugin.js
|
|
801
|
+
// your own plugin: a function of the bus that returns what you want back
|
|
802
|
+
export const countPlugin = (bus) => {
|
|
803
|
+
let count = 0;
|
|
804
|
+
const unbind = bus.on("*", () => {
|
|
805
|
+
count += 1;
|
|
806
|
+
});
|
|
807
|
+
return { count: () => count, stop: unbind };
|
|
808
|
+
};
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
```js
|
|
812
|
+
import { createBus } from "nanoeffects";
|
|
813
|
+
import { countPlugin } from "./count-plugin";
|
|
814
|
+
|
|
815
|
+
const bus = createBus();
|
|
816
|
+
// use — calls countPlugin(bus) and returns its result
|
|
817
|
+
const counter = bus.use(countPlugin);
|
|
818
|
+
|
|
819
|
+
bus.emit("greet", "Ada");
|
|
820
|
+
console.log(counter.count()); // 1
|
|
821
|
+
counter.stop();
|
|
822
|
+
```
|
|
823
|
+
|
|
408
824
|
---
|
|
409
825
|
Source: docs/policies.md
|
|
410
826
|
|
|
@@ -876,14 +1292,28 @@ Recipe without a UI framework: [`examples/modules.ts`](../examples/modules.ts).
|
|
|
876
1292
|
|
|
877
1293
|
## Return a value from `emit` with a command
|
|
878
1294
|
|
|
1295
|
+
The bus lives in one file and its events in another, so any module can import both without a cycle.
|
|
1296
|
+
|
|
879
1297
|
```js
|
|
1298
|
+
// bus.js
|
|
880
1299
|
import { createBus } from "nanoeffects";
|
|
881
|
-
import { createCommand } from "nanoeffects/actions";
|
|
882
1300
|
|
|
883
|
-
|
|
1301
|
+
// createBus — one bus for the app
|
|
1302
|
+
export const bus = createBus();
|
|
1303
|
+
```
|
|
1304
|
+
|
|
1305
|
+
```js
|
|
1306
|
+
// math.js
|
|
1307
|
+
import { createCommand } from "nanoeffects/actions";
|
|
884
1308
|
|
|
885
1309
|
// createCommand — emit runs the handler and returns its result
|
|
886
|
-
const double = createCommand("math/double", (value) => value * 2);
|
|
1310
|
+
export const double = createCommand("math/double", (value) => value * 2);
|
|
1311
|
+
```
|
|
1312
|
+
|
|
1313
|
+
```js
|
|
1314
|
+
import { bus } from "./bus";
|
|
1315
|
+
import { double } from "./math";
|
|
1316
|
+
|
|
887
1317
|
bus.emit(double(21)); // 42
|
|
888
1318
|
```
|
|
889
1319
|
|
|
@@ -892,27 +1322,30 @@ bus.emit(double(21)); // 42
|
|
|
892
1322
|
`nanoeffects/actions` has the pieces to name what a bus does, apart from the code that does it.
|
|
893
1323
|
|
|
894
1324
|
```js
|
|
1325
|
+
// user.js
|
|
895
1326
|
import { createAction, createCommand, createJob } from "nanoeffects/actions";
|
|
896
1327
|
|
|
897
1328
|
// createAction — event with a payload (like RTK createAction)
|
|
898
|
-
const userLoaded = createAction("user/loaded");
|
|
899
|
-
const loadFailed = createAction("user/failed");
|
|
1329
|
+
export const userLoaded = createAction("user/loaded");
|
|
1330
|
+
export const loadFailed = createAction("user/failed");
|
|
900
1331
|
|
|
901
1332
|
// createCommand with a handler — emit returns what the handler returns
|
|
902
|
-
const getUser = createCommand("user/get", (id, { signal }) => {
|
|
1333
|
+
export const getUser = createCommand("user/get", (id, { signal }) => {
|
|
903
1334
|
return fetchUser(id, { signal });
|
|
904
1335
|
});
|
|
905
1336
|
|
|
906
1337
|
// createCommand without a handler — a contract; register the work elsewhere
|
|
907
|
-
const sendMail = createCommand("mail/send");
|
|
1338
|
+
export const sendMail = createCommand("mail/send");
|
|
908
1339
|
|
|
909
1340
|
// createJob — command that also emits success / fail actions
|
|
910
|
-
const loadUser = createJob("user/load", fetchUser, {
|
|
1341
|
+
export const loadUser = createJob("user/load", fetchUser, {
|
|
911
1342
|
success: userLoaded,
|
|
912
1343
|
fail: "user/failed",
|
|
913
1344
|
});
|
|
914
1345
|
```
|
|
915
1346
|
|
|
1347
|
+
These are the events of the user domain, in a file of their own (`user.js`). The bus is made in another file (`bus.js` below), and neither imports the other.
|
|
1348
|
+
|
|
916
1349
|
### Actions
|
|
917
1350
|
|
|
918
1351
|
`createAction(type)` returns a creator. Without generics it keeps the literal type; the bus checks the payload against its event map. Without an event map, give the payload type: `createAction<User>("user/loaded")`.
|
|
@@ -933,22 +1366,22 @@ Calling a command builds `{ type, payload }` like `createAction`. `bus.emit(comm
|
|
|
933
1366
|
**Where handlers are registered.** The command may carry a handler. The bus can also take handlers from a registry (`commandsPlugin`). A handler bound there wins over the command’s own. Each bus (each test) has its own registry.
|
|
934
1367
|
|
|
935
1368
|
```js
|
|
1369
|
+
// bus.js
|
|
936
1370
|
import { createBus } from "nanoeffects";
|
|
937
|
-
import {
|
|
938
|
-
|
|
939
|
-
commandsPlugin,
|
|
940
|
-
createCommand,
|
|
941
|
-
} from "nanoeffects/actions";
|
|
942
|
-
|
|
943
|
-
const sendMail = createCommand("mail/send");
|
|
944
|
-
const getUser = createCommand("user/get");
|
|
1371
|
+
import { bindHandler, commandsPlugin } from "nanoeffects/actions";
|
|
1372
|
+
import { sendMail } from "./user";
|
|
945
1373
|
|
|
946
|
-
const bus = createBus();
|
|
1374
|
+
export const bus = createBus();
|
|
947
1375
|
|
|
948
1376
|
// commandsPlugin — registry of handlers for this bus
|
|
949
|
-
const commands = bus.use(
|
|
1377
|
+
export const commands = bus.use(
|
|
950
1378
|
commandsPlugin([bindHandler(sendMail, smtpSend)]),
|
|
951
1379
|
);
|
|
1380
|
+
```
|
|
1381
|
+
|
|
1382
|
+
```js
|
|
1383
|
+
import { commands } from "./bus";
|
|
1384
|
+
import { getUser } from "./user";
|
|
952
1385
|
|
|
953
1386
|
// later, e.g. in a test — latest binding wins
|
|
954
1387
|
const unbind = commands.handle(getUser, async () => fakeUser);
|
|
@@ -957,7 +1390,7 @@ unbind();
|
|
|
957
1390
|
|
|
958
1391
|
A command with no handler anywhere throws `No handler for command "..."` from `emit` (the call itself only builds the action).
|
|
959
1392
|
|
|
960
|
-
The registry lives in `nanoeffects/actions`, so a bus without one carries none of that code. Without a registry, `emit` runs the command’s own handler. `bus.use(commandsPlugin())` sets `bus.commands`. A second
|
|
1393
|
+
The registry lives in `nanoeffects/actions`, so a bus without one carries none of that code. Without a registry, `emit` runs the command’s own handler. `bus.use(commandsPlugin())` sets `bus.commands`. A second `commandsPlugin` binds its list in that registry and returns it. There is no `commands` option in `createBus`; assign a custom runner with `bus.commands = { run }`.
|
|
961
1394
|
|
|
962
1395
|
### Jobs
|
|
963
1396
|
|
|
@@ -980,18 +1413,20 @@ The registry lives in `nanoeffects/actions`, so a bus without one carries none o
|
|
|
980
1413
|
`bus.use(modulePlugin(modules))` assembles an app from modules and returns `mount`, `unmount`, `isMounted`, `dispose`, `commands`, and `createModule`: the same as `createModule` below, with the context fields of the bus typed (see [TypeScript](typescript.md)).
|
|
981
1414
|
|
|
982
1415
|
```js
|
|
983
|
-
|
|
984
|
-
import {
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1416
|
+
// user.js
|
|
1417
|
+
import { createCommand } from "nanoeffects/actions";
|
|
1418
|
+
|
|
1419
|
+
export const getUser = createCommand("user/get");
|
|
1420
|
+
export const saveUser = createCommand("user/save", persistUser);
|
|
1421
|
+
```
|
|
989
1422
|
|
|
990
|
-
|
|
991
|
-
|
|
1423
|
+
```js
|
|
1424
|
+
// user-module.js
|
|
1425
|
+
import { createModule } from "nanoeffects/actions";
|
|
1426
|
+
import { getUser, saveUser } from "./user";
|
|
992
1427
|
|
|
993
1428
|
// createModule — one domain; unmount undoes everything below
|
|
994
|
-
const userModule = createModule("user", (m) => {
|
|
1429
|
+
export const userModule = createModule("user", (m) => {
|
|
995
1430
|
m.handle(getUser, fetchUser);
|
|
996
1431
|
m.use(saveUser, authModule);
|
|
997
1432
|
m.on("login", (user, ctx) => {
|
|
@@ -1001,10 +1436,22 @@ const userModule = createModule("user", (m) => {
|
|
|
1001
1436
|
/* background work; ends when the module unmounts */
|
|
1002
1437
|
});
|
|
1003
1438
|
});
|
|
1439
|
+
```
|
|
1004
1440
|
|
|
1005
|
-
|
|
1441
|
+
```js
|
|
1442
|
+
// bus.js
|
|
1443
|
+
import { createBus } from "nanoeffects";
|
|
1444
|
+
import { modulePlugin } from "nanoeffects/actions";
|
|
1445
|
+
import { userModule } from "./user-module";
|
|
1446
|
+
|
|
1447
|
+
export const bus = createBus();
|
|
1006
1448
|
// modulePlugin — mount modules on the bus
|
|
1007
|
-
const app = bus.use(modulePlugin([userModule]));
|
|
1449
|
+
export const app = bus.use(modulePlugin([userModule]));
|
|
1450
|
+
```
|
|
1451
|
+
|
|
1452
|
+
```js
|
|
1453
|
+
import { app } from "./bus";
|
|
1454
|
+
import { getUser } from "./user";
|
|
1008
1455
|
|
|
1009
1456
|
const unmount = app.mount(orderModule); // later, e.g. after a lazy import
|
|
1010
1457
|
app.commands.handle(getUser, fakeUser); // in a test
|
|
@@ -1015,6 +1462,7 @@ app.dispose(); // unmounts every module
|
|
|
1015
1462
|
- **A module is known by its name.** Mounting a module whose name is taken replaces the mounted one (see [Hot reload](hmr.md)).
|
|
1016
1463
|
- **Registry.** If `bus.use(commandsPlugin())` ran first, that registry is used; otherwise the module plugin attaches its own. Either way it is `bus.commands` and `app.commands`.
|
|
1017
1464
|
- A disposed app cannot mount modules.
|
|
1465
|
+
- **The given modules are mounted on the first use** of the app or the bus (the first `bus.emit`, or a call of `mount`, `unmount`, `isMounted`, `dispose`, or the read of `commands`), so the plugins you use after `modulePlugin` still reach them. The `bus.on` calls made in between are held and made right after, so the listeners of a module stay ahead of them. See [using plugins in any order](plugins.md#use-them-in-any-order).
|
|
1018
1466
|
- Module context = task context plus `handle`, `use`, and `spawn`:
|
|
1019
1467
|
- `handle` — give a command a handler (contract, or override).
|
|
1020
1468
|
- `use` — bring sub-modules and commands that carry a handler; the returned function undoes that call.
|
|
@@ -1023,20 +1471,32 @@ app.dispose(); // unmounts every module
|
|
|
1023
1471
|
|
|
1024
1472
|
## Give a command, an action or a module its own fields
|
|
1025
1473
|
|
|
1026
|
-
A command can need its own client, a module its own logger. Pass a scope, a plain object whose keys go on `ctx` (
|
|
1474
|
+
A command can need its own client, a module its own logger. Pass a scope, a plain object whose keys go on `ctx` (`$effects` binds functions to the task), as the last argument of `createCommand`, `createAction`, `createModule` and `createJob`, and turn it on with `scopePlugin`:
|
|
1027
1475
|
|
|
1028
1476
|
```js
|
|
1477
|
+
// bus.js
|
|
1029
1478
|
import { createBus } from "nanoeffects";
|
|
1030
|
-
import { createCommand } from "nanoeffects/actions";
|
|
1031
1479
|
import { scopePlugin } from "nanoeffects/scope";
|
|
1032
1480
|
|
|
1033
1481
|
// scopePlugin — makes the scope of a command work on this bus
|
|
1034
|
-
const bus = createBus().use(scopePlugin());
|
|
1482
|
+
export const bus = createBus().use(scopePlugin());
|
|
1483
|
+
```
|
|
1484
|
+
|
|
1485
|
+
```js
|
|
1486
|
+
// user.js
|
|
1487
|
+
import { createCommand } from "nanoeffects/actions";
|
|
1035
1488
|
|
|
1036
1489
|
// the third argument — fields of this command only
|
|
1037
|
-
const getUser = createCommand(
|
|
1038
|
-
|
|
1039
|
-
})
|
|
1490
|
+
export const getUser = createCommand(
|
|
1491
|
+
"user/get",
|
|
1492
|
+
(id, { api }) => api.getUser(id),
|
|
1493
|
+
{ api },
|
|
1494
|
+
);
|
|
1495
|
+
```
|
|
1496
|
+
|
|
1497
|
+
```js
|
|
1498
|
+
import { bus } from "./bus";
|
|
1499
|
+
import { getUser } from "./user";
|
|
1040
1500
|
|
|
1041
1501
|
// a test replaces them for one call
|
|
1042
1502
|
const fakeApi = { getUser: async (id) => ({ id, name: "test" }) };
|
|
@@ -1067,7 +1527,7 @@ If you do not have a store yet, use `createBus` from the [Guide](guide.md).
|
|
|
1067
1527
|
- [React: sharing a value across components](#react-sharing-a-value-across-components)
|
|
1068
1528
|
- [Callbacks on every change](#callbacks-on-every-change)
|
|
1069
1529
|
- [Stop watching a store](#stop-watching-a-store)
|
|
1070
|
-
- [
|
|
1530
|
+
- [Change the store from any handler](#change-the-store-from-any-handler)
|
|
1071
1531
|
|
|
1072
1532
|
## Wire Redux Toolkit to a bus
|
|
1073
1533
|
|
|
@@ -1078,23 +1538,40 @@ Redux’s `dispatch` → reducers updates **state**. It does not give you first-
|
|
|
1078
1538
|
Skip this section if you are not already on Redux. The recipe as a file, type-checked with the project: [`examples/redux.ts`](../examples/redux.ts).
|
|
1079
1539
|
|
|
1080
1540
|
```js
|
|
1081
|
-
|
|
1541
|
+
// bus.js
|
|
1082
1542
|
import { createBus } from "nanoeffects";
|
|
1083
|
-
import { latest } from "nanoeffects/policies";
|
|
1084
|
-
import { createEffectsMiddleware } from "nanoeffects/redux";
|
|
1085
1543
|
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1544
|
+
export const bus = createBus(); // the bus of your app; it can exist before the store
|
|
1545
|
+
```
|
|
1546
|
+
|
|
1547
|
+
```js
|
|
1548
|
+
// user.js
|
|
1549
|
+
import { createAction } from "@reduxjs/toolkit";
|
|
1550
|
+
|
|
1551
|
+
export const userRequested = createAction("user/requested"); // RTK
|
|
1552
|
+
export const userLoaded = createAction("user/loaded");
|
|
1553
|
+
```
|
|
1088
1554
|
|
|
1089
|
-
|
|
1555
|
+
```js
|
|
1556
|
+
// store.js
|
|
1557
|
+
import { configureStore } from "@reduxjs/toolkit";
|
|
1558
|
+
import { createEffectsMiddleware } from "nanoeffects/redux";
|
|
1559
|
+
import { bus } from "./bus";
|
|
1090
1560
|
|
|
1091
1561
|
// createEffectsMiddleware — actions reach the bus after reducers; tasks get ctx.store
|
|
1092
1562
|
const effects = createEffectsMiddleware(bus);
|
|
1093
1563
|
|
|
1094
|
-
const store = configureStore({
|
|
1564
|
+
export const store = configureStore({
|
|
1095
1565
|
reducer,
|
|
1096
1566
|
middleware: (getDefault) => getDefault().concat(effects),
|
|
1097
1567
|
});
|
|
1568
|
+
```
|
|
1569
|
+
|
|
1570
|
+
```js
|
|
1571
|
+
// user-handlers.js
|
|
1572
|
+
import { latest } from "nanoeffects/policies";
|
|
1573
|
+
import { bus } from "./bus";
|
|
1574
|
+
import { userLoaded, userRequested } from "./user";
|
|
1098
1575
|
|
|
1099
1576
|
// latest — a newer run aborts the older one
|
|
1100
1577
|
bus.on(
|
|
@@ -1145,28 +1622,44 @@ const effects = createEffectsMiddleware(bus);
|
|
|
1145
1622
|
Zustand, MobX, Jotai, Valtio, Nanostores and Pinia have no adapter. Redux is different: use the middleware from the first section, it sends actions to the bus. A plain store works with the bus in two directions, and both are shown for each store below:
|
|
1146
1623
|
|
|
1147
1624
|
- **The store triggers an event.** The store tells you about a change with a callback. You pass that callback `bus.emitter(action)`, and the change becomes an event that starts a handler.
|
|
1148
|
-
- **The event changes the store.** The handler reports its result as an event. A listener of the bus
|
|
1625
|
+
- **The event changes the store.** The handler reports its result as an event. A listener of the bus gets the store from `ctx` and changes it with a mutation.
|
|
1149
1626
|
|
|
1150
1627
|
The example is a search box. The store holds two fields: `query`, which the user types, and `results`, which a request fills. A change of `query` starts a debounced search; the search ends with an event that writes `results`.
|
|
1151
1628
|
|
|
1152
1629
|
Writing `(query) => bus.emit(queryChanged(query))` for every subscription repeats the bus and the action. `emitterPlugin` adds `bus.emitter(action)`: it returns that callback for you. The action stays a plain `createAction`, so `bus.emit(queryChanged("nano"))` and `bus.on(queryChanged, …)` work as before.
|
|
1153
1630
|
|
|
1154
|
-
The bus
|
|
1631
|
+
The bus gets the store as context: `contextPlugin({ store })` puts it on every task as `ctx.store` ([Context fields](context.md)). A handler then changes the store with its mutations and never reaches for the store by name. Only `store.js` is different for each store below. It exports a `store` with the same three members:
|
|
1632
|
+
|
|
1633
|
+
- `setQuery(query)` and `setResults(items)` are the mutations. The user's input calls the first, the handler calls the second.
|
|
1634
|
+
- `onQueryChanged(callback)` tells you about a change of `query`, and returns the function that stops it.
|
|
1635
|
+
|
|
1636
|
+
The bus, the events, the handlers and the link from the store to the bus are the same for every store, each in a file of its own: the bus knows no events, and the events know no bus.
|
|
1155
1637
|
|
|
1156
1638
|
```js
|
|
1157
|
-
//
|
|
1158
|
-
import { createBus } from "nanoeffects";
|
|
1159
|
-
import {
|
|
1160
|
-
import {
|
|
1639
|
+
// bus.js
|
|
1640
|
+
import { contextPlugin, createBus } from "nanoeffects";
|
|
1641
|
+
import { emitterPlugin } from "nanoeffects/actions";
|
|
1642
|
+
import { store } from "./store";
|
|
1643
|
+
|
|
1644
|
+
export const bus = createBus()
|
|
1645
|
+
.use(emitterPlugin()) // adds bus.emitter
|
|
1646
|
+
.use(contextPlugin({ store })); // ctx.store on every task
|
|
1647
|
+
```
|
|
1161
1648
|
|
|
1162
|
-
|
|
1649
|
+
```js
|
|
1650
|
+
// search.js
|
|
1651
|
+
import { createAction } from "nanoeffects/actions";
|
|
1163
1652
|
|
|
1164
1653
|
// createAction — plain action creators; calling one only builds { type, payload }
|
|
1165
1654
|
export const queryChanged = createAction("search/queryChanged");
|
|
1166
1655
|
export const searchLoaded = createAction("search/loaded");
|
|
1656
|
+
```
|
|
1167
1657
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1658
|
+
```js
|
|
1659
|
+
// search-handlers.js
|
|
1660
|
+
import { debounce, latest } from "nanoeffects/policies";
|
|
1661
|
+
import { bus } from "./bus";
|
|
1662
|
+
import { queryChanged, searchLoaded } from "./search";
|
|
1170
1663
|
|
|
1171
1664
|
// debounce — wait 300 ms of quiet; latest — abort the search of the previous query
|
|
1172
1665
|
bus.on(
|
|
@@ -1180,141 +1673,199 @@ bus.on(
|
|
|
1180
1673
|
}),
|
|
1181
1674
|
),
|
|
1182
1675
|
);
|
|
1676
|
+
|
|
1677
|
+
// event → store: the store comes from ctx, and a mutation changes it
|
|
1678
|
+
bus.on(searchLoaded, (items, { store }) => {
|
|
1679
|
+
store.setResults(items);
|
|
1680
|
+
});
|
|
1681
|
+
```
|
|
1682
|
+
|
|
1683
|
+
```js
|
|
1684
|
+
// connect.js
|
|
1685
|
+
import { bus } from "./bus";
|
|
1686
|
+
import { queryChanged } from "./search";
|
|
1687
|
+
import { store } from "./store";
|
|
1688
|
+
|
|
1689
|
+
// store → event: bus.emitter turns the callback into an emit of queryChanged
|
|
1690
|
+
export const stopQuery = store.onQueryChanged(bus.emitter(queryChanged));
|
|
1183
1691
|
```
|
|
1184
1692
|
|
|
1185
1693
|
Write `results` and never the field you watch: a handler that changed `query` would start itself again. Find your store below.
|
|
1186
1694
|
|
|
1187
1695
|
### Nanostores
|
|
1188
1696
|
|
|
1189
|
-
`listen` calls back with the new value
|
|
1697
|
+
`action` makes a named mutation of an atom. `listen` calls back with the new value; `subscribe` would also call at once, with the current value, so use `listen`.
|
|
1190
1698
|
|
|
1191
1699
|
```js
|
|
1192
|
-
|
|
1193
|
-
import {
|
|
1700
|
+
// store.js
|
|
1701
|
+
import { action, atom } from "nanostores";
|
|
1194
1702
|
|
|
1195
1703
|
const $query = atom("");
|
|
1196
1704
|
const $results = atom([]);
|
|
1197
1705
|
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
$
|
|
1204
|
-
}
|
|
1706
|
+
export const store = {
|
|
1707
|
+
// action — a named mutation of an atom
|
|
1708
|
+
setQuery: action($query, "setQuery", (target, query) => target.set(query)),
|
|
1709
|
+
setResults: action($results, "setResults", (target, items) => target.set(items)),
|
|
1710
|
+
// listen — calls back with the new query on every change
|
|
1711
|
+
onQueryChanged: (callback) => $query.listen(callback),
|
|
1712
|
+
};
|
|
1205
1713
|
```
|
|
1206
1714
|
|
|
1207
1715
|
### MobX
|
|
1208
1716
|
|
|
1209
|
-
`reaction` watches an expression and calls back with its value.
|
|
1717
|
+
`makeAutoObservable` turns the methods of a class into actions, which is where MobX wants changes to happen. `reaction` watches an expression and calls back with its value.
|
|
1718
|
+
|
|
1719
|
+
<!-- docs-check: fork -->
|
|
1210
1720
|
|
|
1211
1721
|
```js
|
|
1212
|
-
|
|
1213
|
-
import {
|
|
1722
|
+
// store.js
|
|
1723
|
+
import { makeAutoObservable, reaction } from "mobx";
|
|
1214
1724
|
|
|
1215
|
-
|
|
1725
|
+
class SearchStore {
|
|
1726
|
+
query = "";
|
|
1727
|
+
results = [];
|
|
1216
1728
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1729
|
+
constructor() {
|
|
1730
|
+
makeAutoObservable(this); // the methods below become actions
|
|
1731
|
+
}
|
|
1219
1732
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1733
|
+
setQuery(query) {
|
|
1734
|
+
this.query = query;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
setResults(items) {
|
|
1738
|
+
this.results = items;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// reaction — watches the expression and calls back with its value
|
|
1742
|
+
onQueryChanged(callback) {
|
|
1743
|
+
return reaction(() => this.query, callback);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
export const store = new SearchStore();
|
|
1226
1748
|
```
|
|
1227
1749
|
|
|
1228
1750
|
### Valtio
|
|
1229
1751
|
|
|
1230
|
-
`subscribeKey` watches one field of a proxy state.
|
|
1752
|
+
`subscribeKey` watches one field of a proxy state. A mutation assigns to the proxy.
|
|
1753
|
+
|
|
1754
|
+
<!-- docs-check: fork -->
|
|
1231
1755
|
|
|
1232
1756
|
```js
|
|
1757
|
+
// store.js
|
|
1233
1758
|
import { proxy } from "valtio";
|
|
1234
1759
|
import { subscribeKey } from "valtio/utils";
|
|
1235
|
-
import { bus, emitQueryChanged, searchLoaded } from "./search-bus";
|
|
1236
1760
|
|
|
1237
1761
|
const state = proxy({ query: "", results: [] });
|
|
1238
1762
|
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
}
|
|
1763
|
+
export const store = {
|
|
1764
|
+
setQuery(query) {
|
|
1765
|
+
state.query = query;
|
|
1766
|
+
},
|
|
1767
|
+
setResults(items) {
|
|
1768
|
+
state.results = items;
|
|
1769
|
+
},
|
|
1770
|
+
// subscribeKey — watches one field
|
|
1771
|
+
onQueryChanged: (callback) => subscribeKey(state, "query", callback),
|
|
1772
|
+
};
|
|
1246
1773
|
```
|
|
1247
1774
|
|
|
1248
1775
|
### Jotai
|
|
1249
1776
|
|
|
1250
|
-
`sub` gives no value, so read it yourself.
|
|
1777
|
+
`sub` gives no value, so read it yourself. A write-only atom is the mutation, and `set` runs it.
|
|
1778
|
+
|
|
1779
|
+
<!-- docs-check: fork -->
|
|
1251
1780
|
|
|
1252
1781
|
```js
|
|
1782
|
+
// store.js
|
|
1253
1783
|
import { atom, createStore } from "jotai";
|
|
1254
|
-
import { bus, emitQueryChanged, searchLoaded } from "./search-bus";
|
|
1255
1784
|
|
|
1256
1785
|
const queryAtom = atom("");
|
|
1257
1786
|
const resultsAtom = atom([]);
|
|
1258
|
-
const
|
|
1259
|
-
|
|
1260
|
-
//
|
|
1261
|
-
const
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
const
|
|
1265
|
-
|
|
1266
|
-
|
|
1787
|
+
const jotai = createStore();
|
|
1788
|
+
|
|
1789
|
+
// write-only atoms: the mutations
|
|
1790
|
+
const setQueryAtom = atom(null, (_get, set, query) => set(queryAtom, query));
|
|
1791
|
+
const setResultsAtom = atom(null, (_get, set, items) => set(resultsAtom, items));
|
|
1792
|
+
|
|
1793
|
+
export const store = {
|
|
1794
|
+
setQuery: (query) => jotai.set(setQueryAtom, query),
|
|
1795
|
+
setResults: (items) => jotai.set(setResultsAtom, items),
|
|
1796
|
+
// sub calls back without a value; get reads it
|
|
1797
|
+
onQueryChanged: (callback) =>
|
|
1798
|
+
jotai.sub(queryAtom, () => callback(jotai.get(queryAtom))),
|
|
1799
|
+
};
|
|
1267
1800
|
```
|
|
1268
1801
|
|
|
1269
1802
|
### Zustand
|
|
1270
1803
|
|
|
1271
|
-
With `subscribeWithSelector` the selector picks the field, and the callback is called only when it changes. Without a selector Zustand calls back on every `setState`, with the whole state.
|
|
1804
|
+
With `subscribeWithSelector` the selector picks the field, and the callback is called only when it changes. Without a selector Zustand calls back on every `setState`, with the whole state. The mutations are functions in the state.
|
|
1805
|
+
|
|
1806
|
+
<!-- docs-check: fork -->
|
|
1272
1807
|
|
|
1273
1808
|
```js
|
|
1809
|
+
// store.js
|
|
1274
1810
|
import { create } from "zustand";
|
|
1275
1811
|
import { subscribeWithSelector } from "zustand/middleware";
|
|
1276
|
-
import { bus, emitQueryChanged, searchLoaded } from "./search-bus";
|
|
1277
1812
|
|
|
1278
|
-
const
|
|
1279
|
-
subscribeWithSelector(() => ({
|
|
1813
|
+
const useSearch = create(
|
|
1814
|
+
subscribeWithSelector((set) => ({
|
|
1815
|
+
query: "",
|
|
1816
|
+
results: [],
|
|
1817
|
+
// the mutations are part of the state
|
|
1818
|
+
setQuery: (query) => set({ query }),
|
|
1819
|
+
setResults: (items) => set({ results: items }),
|
|
1820
|
+
})),
|
|
1280
1821
|
);
|
|
1281
1822
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
//
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
});
|
|
1823
|
+
export const store = {
|
|
1824
|
+
setQuery: (query) => useSearch.getState().setQuery(query),
|
|
1825
|
+
setResults: (items) => useSearch.getState().setResults(items),
|
|
1826
|
+
// a selector: called back with state.query only when it changes
|
|
1827
|
+
onQueryChanged: (callback) => useSearch.subscribe((state) => state.query, callback),
|
|
1828
|
+
};
|
|
1289
1829
|
```
|
|
1290
1830
|
|
|
1291
1831
|
### Pinia
|
|
1292
1832
|
|
|
1293
|
-
Pinia (the store of Vue) keeps reactive objects, and its `$subscribe` reports every mutation of the store, not one field. Watch one getter with Vue's `watch` instead: it calls back with the new value.
|
|
1833
|
+
Pinia (the store of Vue) keeps reactive objects, and its `$subscribe` reports every mutation of the store, not one field. Watch one getter with Vue's `watch` instead: it calls back with the new value. The mutations are the actions of the store.
|
|
1834
|
+
|
|
1835
|
+
<!-- docs-check: fork -->
|
|
1294
1836
|
|
|
1295
1837
|
```js
|
|
1296
|
-
|
|
1838
|
+
// store.js
|
|
1297
1839
|
import { defineStore } from "pinia";
|
|
1298
|
-
import {
|
|
1840
|
+
import { watch } from "vue";
|
|
1299
1841
|
|
|
1300
|
-
// defineStore — Pinia: the state of one store
|
|
1842
|
+
// defineStore — Pinia: the state and the actions of one store
|
|
1301
1843
|
const useSearch = defineStore("search", {
|
|
1302
1844
|
state: () => ({ query: "", results: [] }),
|
|
1845
|
+
actions: {
|
|
1846
|
+
setQuery(query) {
|
|
1847
|
+
this.query = query;
|
|
1848
|
+
},
|
|
1849
|
+
setResults(items) {
|
|
1850
|
+
this.results = items;
|
|
1851
|
+
},
|
|
1852
|
+
},
|
|
1303
1853
|
});
|
|
1304
1854
|
|
|
1305
1855
|
const search = useSearch(); // call it after the app has installed Pinia
|
|
1306
1856
|
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
//
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
});
|
|
1857
|
+
export const store = {
|
|
1858
|
+
setQuery: (query) => search.setQuery(query),
|
|
1859
|
+
setResults: (items) => search.setResults(items),
|
|
1860
|
+
// watch — calls back with the new query when search.query changes
|
|
1861
|
+
onQueryChanged: (callback) => watch(() => search.query, callback),
|
|
1862
|
+
};
|
|
1314
1863
|
```
|
|
1315
1864
|
|
|
1316
1865
|
### React: sharing a value across components
|
|
1317
1866
|
|
|
1867
|
+
<!-- docs-check: new -->
|
|
1868
|
+
|
|
1318
1869
|
A value of your UI (a query, a filter, a name) often has two jobs. Several components read it and re-render when it changes, and the bus must hear about each change to start a handler. The recipes below differ in where the value lives. Pick the first one unless a handler must read the value.
|
|
1319
1870
|
|
|
1320
1871
|
| The value lives in | Use when |
|
|
@@ -1327,20 +1878,30 @@ A value of your UI (a query, a filter, a name) often has two jobs. Several compo
|
|
|
1327
1878
|
|
|
1328
1879
|
Writing `useEffect(() => bus.emit(...), [value])` looks enough, but it emits on mount, emits twice on mount under StrictMode, and re-runs when the emit function is new on every render. `useEmitter` from `nanoeffects/react` avoids all three. It emits after a render commits with a changed value, never on mount, and compares with `Object.is`. You need React 18 or newer; no other entry of the package uses React.
|
|
1329
1880
|
|
|
1330
|
-
First the bus. `reactPlugin()` adds `bus.useEmitter`, and destructuring it gives a plain hook that you export:
|
|
1881
|
+
First the bus, the events and the handlers, each in its own file. `reactPlugin()` adds `bus.useEmitter`, and destructuring it gives a plain hook that you export from the bus file:
|
|
1331
1882
|
|
|
1332
1883
|
```js
|
|
1333
|
-
//
|
|
1884
|
+
// bus.js
|
|
1334
1885
|
import { createBus } from "nanoeffects";
|
|
1335
|
-
import { createAction } from "nanoeffects/actions";
|
|
1336
|
-
import { debounce, latest } from "nanoeffects/policies";
|
|
1337
1886
|
import { reactPlugin } from "nanoeffects/react";
|
|
1338
1887
|
|
|
1339
1888
|
export const bus = createBus().use(reactPlugin()); // the bus; reactPlugin adds bus.useEmitter
|
|
1340
1889
|
export const { useEmitter } = bus; // a free hook: it does not read `this`
|
|
1890
|
+
```
|
|
1891
|
+
|
|
1892
|
+
```js
|
|
1893
|
+
// search.js
|
|
1894
|
+
import { createAction } from "nanoeffects/actions";
|
|
1341
1895
|
|
|
1342
1896
|
export const queryChanged = createAction("query/changed");
|
|
1343
1897
|
export const searchLoaded = createAction("search/loaded");
|
|
1898
|
+
```
|
|
1899
|
+
|
|
1900
|
+
```js
|
|
1901
|
+
// search-handlers.js
|
|
1902
|
+
import { debounce, latest } from "nanoeffects/policies";
|
|
1903
|
+
import { bus } from "./bus";
|
|
1904
|
+
import { queryChanged, searchLoaded } from "./search";
|
|
1344
1905
|
|
|
1345
1906
|
// debounce — wait 300 ms of quiet; latest — a newer query aborts the older request
|
|
1346
1907
|
bus.on(
|
|
@@ -1360,7 +1921,8 @@ Then the provider. It holds the state, calls `useEmitter` once, and listens for
|
|
|
1360
1921
|
```jsx
|
|
1361
1922
|
// SearchProvider.jsx
|
|
1362
1923
|
import { createContext, useContext, useEffect, useState } from "react";
|
|
1363
|
-
import { bus,
|
|
1924
|
+
import { bus, useEmitter } from "./bus";
|
|
1925
|
+
import { queryChanged, searchLoaded } from "./search";
|
|
1364
1926
|
|
|
1365
1927
|
const SearchContext = createContext(null);
|
|
1366
1928
|
|
|
@@ -1417,6 +1979,8 @@ Two limits to know:
|
|
|
1417
1979
|
|
|
1418
1980
|
A context is not visible to a bus handler. When a handler needs the current value, keep the value outside React. About twelve lines of `useSyncExternalStore` do it, and `contextPlugin` hands it to every handler as `ctx.query`:
|
|
1419
1981
|
|
|
1982
|
+
<!-- docs-check: new -->
|
|
1983
|
+
|
|
1420
1984
|
```jsx
|
|
1421
1985
|
// value-store.js
|
|
1422
1986
|
import { useSyncExternalStore } from "react";
|
|
@@ -1446,19 +2010,36 @@ export function createValue(initial) {
|
|
|
1446
2010
|
export const useValue = (store) => useSyncExternalStore(store.subscribe, store.get);
|
|
1447
2011
|
```
|
|
1448
2012
|
|
|
1449
|
-
```
|
|
1450
|
-
//
|
|
1451
|
-
import {
|
|
1452
|
-
import { createAction, emitterPlugin } from "nanoeffects/actions";
|
|
1453
|
-
import { createValue, useValue } from "./value-store";
|
|
2013
|
+
```js
|
|
2014
|
+
// query.js
|
|
2015
|
+
import { createValue } from "./value-store";
|
|
1454
2016
|
|
|
1455
|
-
const query = createValue("");
|
|
2017
|
+
export const query = createValue("");
|
|
2018
|
+
```
|
|
2019
|
+
|
|
2020
|
+
```js
|
|
2021
|
+
// bus.js
|
|
2022
|
+
import { contextPlugin, createBus } from "nanoeffects";
|
|
2023
|
+
import { emitterPlugin } from "nanoeffects/actions";
|
|
2024
|
+
import { query } from "./query";
|
|
1456
2025
|
|
|
1457
2026
|
// emitterPlugin adds bus.emitter; contextPlugin puts `query` on every task as ctx.query
|
|
1458
|
-
const bus = createBus().use(emitterPlugin()).use(contextPlugin({ query }));
|
|
2027
|
+
export const bus = createBus().use(emitterPlugin()).use(contextPlugin({ query }));
|
|
2028
|
+
```
|
|
1459
2029
|
|
|
1460
|
-
|
|
1461
|
-
|
|
2030
|
+
```js
|
|
2031
|
+
// search.js
|
|
2032
|
+
import { createAction } from "nanoeffects/actions";
|
|
2033
|
+
|
|
2034
|
+
export const queryChanged = createAction("query/changed");
|
|
2035
|
+
export const refreshed = createAction("search/refreshed");
|
|
2036
|
+
```
|
|
2037
|
+
|
|
2038
|
+
```js
|
|
2039
|
+
// search-handlers.js
|
|
2040
|
+
import { bus } from "./bus";
|
|
2041
|
+
import { query } from "./query";
|
|
2042
|
+
import { queryChanged, refreshed } from "./search";
|
|
1462
2043
|
|
|
1463
2044
|
// store → bus: bus.emitter turns the action into a callback; one place, any number of setters
|
|
1464
2045
|
const emitQueryChanged = bus.emitter(queryChanged);
|
|
@@ -1469,6 +2050,12 @@ bus.on(refreshed, async (_payload, ctx) => {
|
|
|
1469
2050
|
const items = await api.search(ctx.query.get(), { signal: ctx.signal });
|
|
1470
2051
|
console.log(items.length);
|
|
1471
2052
|
});
|
|
2053
|
+
```
|
|
2054
|
+
|
|
2055
|
+
```jsx
|
|
2056
|
+
// SearchBox.jsx
|
|
2057
|
+
import { query } from "./query";
|
|
2058
|
+
import { useValue } from "./value-store";
|
|
1472
2059
|
|
|
1473
2060
|
// any number of components read and write the same value
|
|
1474
2061
|
export function SearchBox() {
|
|
@@ -1481,16 +2068,31 @@ export function SearchBox() {
|
|
|
1481
2068
|
|
|
1482
2069
|
With Nanostores, Jotai or Zustand the value already lives outside React and has a subscription. Connect it to the bus once, as in [Nanostores](#nanostores), and read it in components with the library's own hook:
|
|
1483
2070
|
|
|
2071
|
+
<!-- docs-check: new -->
|
|
2072
|
+
|
|
2073
|
+
```js
|
|
2074
|
+
// bus.js
|
|
2075
|
+
import { createBus } from "nanoeffects";
|
|
2076
|
+
import { emitterPlugin } from "nanoeffects/actions";
|
|
2077
|
+
|
|
2078
|
+
export const bus = createBus().use(emitterPlugin()); // adds bus.emitter
|
|
2079
|
+
```
|
|
2080
|
+
|
|
2081
|
+
```js
|
|
2082
|
+
// search.js
|
|
2083
|
+
import { createAction } from "nanoeffects/actions";
|
|
2084
|
+
|
|
2085
|
+
export const queryChanged = createAction("query/changed");
|
|
2086
|
+
```
|
|
2087
|
+
|
|
1484
2088
|
```jsx
|
|
1485
|
-
//
|
|
2089
|
+
// SearchBox.jsx
|
|
1486
2090
|
import { atom } from "nanostores";
|
|
1487
2091
|
import { useStore } from "@nanostores/react";
|
|
1488
|
-
import {
|
|
1489
|
-
import {
|
|
2092
|
+
import { bus } from "./bus";
|
|
2093
|
+
import { queryChanged } from "./search";
|
|
1490
2094
|
|
|
1491
|
-
const bus = createBus().use(emitterPlugin()); // adds bus.emitter
|
|
1492
2095
|
const $query = atom("");
|
|
1493
|
-
const queryChanged = createAction("query/changed");
|
|
1494
2096
|
|
|
1495
2097
|
// listen, not subscribe: listen skips the call with the current value at start
|
|
1496
2098
|
$query.listen(bus.emitter(queryChanged));
|
|
@@ -1505,16 +2107,28 @@ export function SearchBox() {
|
|
|
1505
2107
|
|
|
1506
2108
|
React may run a reducer more than once for one dispatch (twice in StrictMode), so a `bus.emit` inside it fires twice. Dispatch to the reducer first, then emit, from one helper. Or keep the state in a reducer and pass it to `useEmitter` like any other value.
|
|
1507
2109
|
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
2110
|
+
<!-- docs-check: new -->
|
|
2111
|
+
|
|
2112
|
+
```js
|
|
2113
|
+
// bus.js
|
|
1511
2114
|
import { createBus } from "nanoeffects";
|
|
2115
|
+
|
|
2116
|
+
export const bus = createBus(); // the bus: created once
|
|
2117
|
+
```
|
|
2118
|
+
|
|
2119
|
+
```js
|
|
2120
|
+
// search.js
|
|
1512
2121
|
import { createAction } from "nanoeffects/actions";
|
|
1513
2122
|
|
|
1514
|
-
const
|
|
2123
|
+
export const queryChanged = createAction("query/changed");
|
|
2124
|
+
export const queryCleared = createAction("query/cleared");
|
|
2125
|
+
```
|
|
1515
2126
|
|
|
1516
|
-
|
|
1517
|
-
|
|
2127
|
+
```jsx
|
|
2128
|
+
// Search.jsx
|
|
2129
|
+
import { useReducer } from "react";
|
|
2130
|
+
import { bus } from "./bus";
|
|
2131
|
+
import { queryChanged, queryCleared } from "./search";
|
|
1518
2132
|
|
|
1519
2133
|
function reducer(state, action) {
|
|
1520
2134
|
if (queryChanged.match(action)) {
|
|
@@ -1554,11 +2168,11 @@ The stores above call back only when the value changed, so the emitter does not
|
|
|
1554
2168
|
|
|
1555
2169
|
### Stop watching a store
|
|
1556
2170
|
|
|
1557
|
-
|
|
2171
|
+
`connect.js` keeps the function that `onQueryChanged` returned (`stopQuery`). Call it yourself, or hand it over: `onAbort(ctx, stopQuery)` inside a task, or `m.signal.addEventListener("abort", stopQuery)` in a module. To stop the other direction, remove the listener of `searchLoaded` with `bus.off(searchLoaded, handler)`, or register it in a module, which removes it on unmount.
|
|
1558
2172
|
|
|
1559
|
-
###
|
|
2173
|
+
### Change the store from any handler
|
|
1560
2174
|
|
|
1561
|
-
|
|
2175
|
+
A handler gets the store from `ctx.store` and calls a mutation, whatever it handles: not only the result of a search. The same rule holds: write the field you do not watch. For a MobX `flow`, `onAbort(ctx, () => promise.cancel())` cancels it with the task.
|
|
1562
2176
|
|
|
1563
2177
|
---
|
|
1564
2178
|
Source: docs/react.md
|
|
@@ -1581,19 +2195,26 @@ Each has a policy or an effect: `latest`, `leading`, `retry`. They live in one p
|
|
|
1581
2195
|
|
|
1582
2196
|
## Wire the bus once
|
|
1583
2197
|
|
|
1584
|
-
|
|
2198
|
+
Keep the bus in a file of its own, and the handlers of the form in another. Create the bus once, outside any component: a bus created in the body of a component would be a new bus, without subscribers, on every render.
|
|
1585
2199
|
|
|
1586
2200
|
```js
|
|
1587
|
-
//
|
|
2201
|
+
// bus.js
|
|
1588
2202
|
import { createBus } from "nanoeffects";
|
|
2203
|
+
|
|
2204
|
+
export const bus = createBus(); // the bus: created once, at the top of the module
|
|
2205
|
+
```
|
|
2206
|
+
|
|
2207
|
+
The handlers know the bus and the bus knows nothing of them. They register when `registration.js` is imported, once, when the app starts:
|
|
2208
|
+
|
|
2209
|
+
```js
|
|
2210
|
+
// registration.js
|
|
1589
2211
|
import { retry } from "nanoeffects/effects";
|
|
1590
2212
|
import { debounce, latest, leading } from "nanoeffects/policies";
|
|
2213
|
+
import { bus } from "./bus";
|
|
1591
2214
|
|
|
1592
2215
|
// fetch rejects with a TypeError when the network fails; an HTTP error is a plain Error
|
|
1593
2216
|
const isNetworkError = (error) => error instanceof TypeError;
|
|
1594
2217
|
|
|
1595
|
-
export const bus = createBus(); // the bus: created once, at the top of the module
|
|
1596
|
-
|
|
1597
2218
|
// debounce — wait 300 ms of quiet; latest — abort the check of the previous name
|
|
1598
2219
|
bus.on(
|
|
1599
2220
|
"username/typed",
|
|
@@ -1631,12 +2252,15 @@ The handlers know nothing about React. A test can emit `username/typed` and read
|
|
|
1631
2252
|
|
|
1632
2253
|
## Connect the form
|
|
1633
2254
|
|
|
2255
|
+
<!-- docs-check: continue -->
|
|
2256
|
+
|
|
1634
2257
|
The component keeps what it shows in `useState`. It subscribes in `useEffect` and emits events from its handlers. `bus.on` returns the function that unsubscribes; returning it from the effect removes the listener on unmount.
|
|
1635
2258
|
|
|
1636
2259
|
```jsx
|
|
1637
2260
|
// SignUpForm.jsx
|
|
1638
2261
|
import { useEffect, useState } from "react";
|
|
1639
|
-
import { bus } from "./
|
|
2262
|
+
import { bus } from "./bus"; // the bus of the previous snippet
|
|
2263
|
+
import "./registration"; // registers the handlers; the app does this once, when it starts
|
|
1640
2264
|
|
|
1641
2265
|
export function SignUpForm() {
|
|
1642
2266
|
const [name, setName] = useState("");
|
|
@@ -1699,6 +2323,8 @@ In development, React StrictMode runs the effect twice (subscribe, unsubscribe,
|
|
|
1699
2323
|
|
|
1700
2324
|
## Stop running work when the form closes
|
|
1701
2325
|
|
|
2326
|
+
<!-- docs-check: continue -->
|
|
2327
|
+
|
|
1702
2328
|
Unsubscribing stops the component from hearing results, but a request that has started keeps running. To cancel it, the component emits `form/closed` when it unmounts, and each handler that should stop listens for it with `ctx.on`:
|
|
1703
2329
|
|
|
1704
2330
|
```js
|
|
@@ -1790,7 +2416,7 @@ Source: docs/scope.md
|
|
|
1790
2416
|
|
|
1791
2417
|
Optional, for domain logic. Use it when one command needs its own client, one module its own logger, or one test its own fake, and a field on the whole bus ([context fields](context.md)) is too wide.
|
|
1792
2418
|
|
|
1793
|
-
A **scope** is a plain object. Every key goes on `ctx` as it is (`cache`, `api`, `logger`), except one:
|
|
2419
|
+
A **scope** is a plain object. Every key goes on `ctx` as it is (`cache`, `api`, `logger`), except one: `$effects`. Its functions take `{ signal }` as the first parameter, like `retry(ctx, fn)`, and go on `ctx` bound to the task, so you call `ctx.fetchJson(url)` instead of `fetchJson(ctx, url)`.
|
|
1794
2420
|
|
|
1795
2421
|
You give a scope to the thing that uses it. A call can replace it. Nothing else changes: handlers still get `(payload, ctx)`.
|
|
1796
2422
|
|
|
@@ -1799,27 +2425,29 @@ You give a scope to the thing that uses it. A call can replace it. Nothing else
|
|
|
1799
2425
|
Scopes are a plugin, so a bus that does not use them pays nothing.
|
|
1800
2426
|
|
|
1801
2427
|
```js
|
|
2428
|
+
// bus.js
|
|
1802
2429
|
import { createBus } from "nanoeffects";
|
|
1803
2430
|
import { scopePlugin } from "nanoeffects/scope";
|
|
1804
2431
|
|
|
1805
2432
|
// scopePlugin — makes the scopes below work on this bus; returns the same bus
|
|
1806
|
-
const bus = createBus().use(scopePlugin());
|
|
2433
|
+
export const bus = createBus().use(scopePlugin());
|
|
1807
2434
|
```
|
|
1808
2435
|
|
|
2436
|
+
The bus is in one file and the commands and actions in files named after their domain, so the next example imports `bus` from here. Each section after it keeps its own `bus.js` and domain file, as if it were an app of its own.
|
|
2437
|
+
|
|
1809
2438
|
- Without `scopePlugin` a scope is **ignored**: no error and no warning. If a field is missing from `ctx`, check this first.
|
|
1810
|
-
- Use it
|
|
2439
|
+
- Use it in any order with the other plugins: a scope wins over `effectsPlugin`, `contextPlugin` and `createBus({ context })`.
|
|
1811
2440
|
|
|
1812
2441
|
## Give a command its own fields
|
|
1813
2442
|
|
|
2443
|
+
<!-- docs-check: continue -->
|
|
2444
|
+
|
|
1814
2445
|
```js
|
|
1815
|
-
|
|
2446
|
+
// user.js
|
|
1816
2447
|
import { createCommand } from "nanoeffects/actions";
|
|
1817
|
-
import { scopePlugin } from "nanoeffects/scope";
|
|
1818
|
-
|
|
1819
|
-
const bus = createBus().use(scopePlugin());
|
|
1820
2448
|
|
|
1821
2449
|
// createCommand — third argument is the scope of this command
|
|
1822
|
-
const getUser = createCommand(
|
|
2450
|
+
export const getUser = createCommand(
|
|
1823
2451
|
"user/get",
|
|
1824
2452
|
(id, { api, cache }) => {
|
|
1825
2453
|
// api and cache are on ctx for this command only
|
|
@@ -1827,6 +2455,11 @@ const getUser = createCommand(
|
|
|
1827
2455
|
},
|
|
1828
2456
|
{ api, cache: new Map() },
|
|
1829
2457
|
);
|
|
2458
|
+
```
|
|
2459
|
+
|
|
2460
|
+
```js
|
|
2461
|
+
import { bus } from "./bus";
|
|
2462
|
+
import { getUser } from "./user";
|
|
1830
2463
|
|
|
1831
2464
|
const user = await bus.emit(getUser("1")); // the handler has api and cache
|
|
1832
2465
|
```
|
|
@@ -1835,21 +2468,18 @@ The listeners of `"user/get"` get the same fields when the command is emitted, b
|
|
|
1835
2468
|
|
|
1836
2469
|
## Bind an effect to the task
|
|
1837
2470
|
|
|
1838
|
-
An effect is a function with `{ signal }` as its first parameter. In
|
|
2471
|
+
An effect is a function with `{ signal }` as its first parameter. In `$effects` it is bound to the task, so it stops with the task.
|
|
1839
2472
|
|
|
1840
2473
|
```js
|
|
1841
|
-
|
|
2474
|
+
// feed.js
|
|
1842
2475
|
import { createCommand } from "nanoeffects/actions";
|
|
1843
2476
|
import { delay, race } from "nanoeffects/effects";
|
|
1844
|
-
import { scopePlugin } from "nanoeffects/scope";
|
|
1845
|
-
|
|
1846
|
-
const bus = createBus().use(scopePlugin());
|
|
1847
2477
|
|
|
1848
2478
|
// your own effect: first parameter is the task (only its signal is used)
|
|
1849
2479
|
const fetchJson = (ctx, url) =>
|
|
1850
2480
|
fetch(url, { signal: ctx.signal }).then((response) => response.json());
|
|
1851
2481
|
|
|
1852
|
-
const loadFeed = createCommand(
|
|
2482
|
+
export const loadFeed = createCommand(
|
|
1853
2483
|
"feed/load",
|
|
1854
2484
|
(url, { fetchJson, race, delay }) =>
|
|
1855
2485
|
// race, delay, fetchJson — bound to this task: no ctx argument
|
|
@@ -1857,30 +2487,56 @@ const loadFeed = createCommand(
|
|
|
1857
2487
|
feed: () => fetchJson(url),
|
|
1858
2488
|
timeout: () => delay(5000),
|
|
1859
2489
|
}),
|
|
1860
|
-
// effects — built-in and your own functions share one object
|
|
1861
|
-
{ effects: { fetchJson, race, delay } },
|
|
2490
|
+
// $effects — built-in and your own functions share one object
|
|
2491
|
+
{ $effects: { fetchJson, race, delay } },
|
|
1862
2492
|
);
|
|
2493
|
+
```
|
|
2494
|
+
|
|
2495
|
+
```js
|
|
2496
|
+
// bus.js
|
|
2497
|
+
import { createBus } from "nanoeffects";
|
|
2498
|
+
import { scopePlugin } from "nanoeffects/scope";
|
|
2499
|
+
|
|
2500
|
+
export const bus = createBus().use(scopePlugin());
|
|
2501
|
+
```
|
|
2502
|
+
|
|
2503
|
+
```js
|
|
2504
|
+
import { bus } from "./bus";
|
|
2505
|
+
import { loadFeed } from "./feed";
|
|
1863
2506
|
|
|
1864
2507
|
const result = await bus.emit(loadFeed("/feed"));
|
|
1865
2508
|
```
|
|
1866
2509
|
|
|
1867
|
-
- Put in
|
|
1868
|
-
- A function under any other key is copied as it is; a function in
|
|
1869
|
-
-
|
|
2510
|
+
- Put in `$effects` only functions that take `ctx` first. A plain function (a formatter, a logger) belongs in `context`.
|
|
2511
|
+
- A function under any other key is copied as it is; a function in `$effects` is bound.
|
|
2512
|
+
- `$effects` is the one name with a meaning. A field called `effects` is an ordinary field: `{ effects: { retries: 3 } }` puts that object on `ctx`, and nothing in it is bound.
|
|
1870
2513
|
|
|
1871
2514
|
## Replace a field for one call
|
|
1872
2515
|
|
|
1873
2516
|
The second argument of `emit` is a scope for the tasks of **that** call. It is the usual way to give a test a fake.
|
|
1874
2517
|
|
|
1875
2518
|
```js
|
|
2519
|
+
// bus.js
|
|
1876
2520
|
import { createBus } from "nanoeffects";
|
|
1877
|
-
import { createCommand } from "nanoeffects/actions";
|
|
1878
2521
|
import { scopePlugin } from "nanoeffects/scope";
|
|
1879
2522
|
|
|
1880
|
-
const bus = createBus().use(scopePlugin());
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
2523
|
+
export const bus = createBus().use(scopePlugin());
|
|
2524
|
+
```
|
|
2525
|
+
|
|
2526
|
+
```js
|
|
2527
|
+
// user.js
|
|
2528
|
+
import { createCommand } from "nanoeffects/actions";
|
|
2529
|
+
|
|
2530
|
+
export const getUser = createCommand(
|
|
2531
|
+
"user/get",
|
|
2532
|
+
(id, { api }) => api.getUser(id),
|
|
2533
|
+
{ api },
|
|
2534
|
+
);
|
|
2535
|
+
```
|
|
2536
|
+
|
|
2537
|
+
```js
|
|
2538
|
+
import { bus } from "./bus";
|
|
2539
|
+
import { getUser } from "./user";
|
|
1884
2540
|
|
|
1885
2541
|
// the real api for this call
|
|
1886
2542
|
await bus.emit(getUser("1"));
|
|
@@ -1894,29 +2550,36 @@ Rules for the second argument:
|
|
|
1894
2550
|
|
|
1895
2551
|
- It works with an action or a command call (`emit(getUser("1"), scope)`, `ctx.emit(loaded(user), scope)`).
|
|
1896
2552
|
- It does not work with the string form: in `emit("user/get", payload)` the second argument is the payload.
|
|
1897
|
-
- It can replace
|
|
2553
|
+
- It can replace `$effects` too: `{ $effects: { delay: instantDelay } }`.
|
|
1898
2554
|
- Fields of the library (`signal`, `emit`, …) in it are ignored.
|
|
1899
2555
|
|
|
1900
2556
|
## Give an action, a job or a module its own fields
|
|
1901
2557
|
|
|
1902
2558
|
```js
|
|
1903
|
-
|
|
2559
|
+
// user.js
|
|
2560
|
+
import { createAction, createJob } from "nanoeffects/actions";
|
|
1904
2561
|
|
|
1905
2562
|
// createAction — the listeners of this action's calls get the fields
|
|
1906
|
-
const userLoaded = createAction("user/loaded", { store });
|
|
2563
|
+
export const userLoaded = createAction("user/loaded", { store });
|
|
1907
2564
|
|
|
1908
2565
|
// createJob — the scope is the fourth argument, after the outcomes;
|
|
1909
2566
|
// the handler and the listeners of the outcome get its fields
|
|
1910
|
-
const loadUser = createJob(
|
|
2567
|
+
export const loadUser = createJob(
|
|
1911
2568
|
"user/load",
|
|
1912
2569
|
fetchUser,
|
|
1913
2570
|
{ success: userLoaded, fail: "user/failed" },
|
|
1914
2571
|
{ logger },
|
|
1915
2572
|
);
|
|
2573
|
+
```
|
|
2574
|
+
|
|
2575
|
+
```js
|
|
2576
|
+
// audit-module.js
|
|
2577
|
+
import { createModule } from "nanoeffects/actions";
|
|
2578
|
+
import { userLoaded } from "./user";
|
|
1916
2579
|
|
|
1917
2580
|
// createModule — third argument; everything the module owns gets the fields:
|
|
1918
2581
|
// m.on listeners, m.handle handlers, m.spawn tasks
|
|
1919
|
-
const auditModule = createModule(
|
|
2582
|
+
export const auditModule = createModule(
|
|
1920
2583
|
"audit",
|
|
1921
2584
|
(m) => {
|
|
1922
2585
|
m.on(userLoaded, (user, { logger }) => logger.info(user.id));
|
|
@@ -1925,6 +2588,8 @@ const auditModule = createModule(
|
|
|
1925
2588
|
);
|
|
1926
2589
|
```
|
|
1927
2590
|
|
|
2591
|
+
If the outcome action (`success` or `fail`) has a scope of its own, its listeners see the job's fields with the action's own fields over them; the handler of the job sees only the job's.
|
|
2592
|
+
|
|
1928
2593
|
A module scope does not leak: a listener registered outside the module does not see it.
|
|
1929
2594
|
|
|
1930
2595
|
## Which value wins
|
|
@@ -1942,25 +2607,26 @@ The fields of the library (`signal`, `abort`, `emit`, `on`, `off`, `run`, `type`
|
|
|
1942
2607
|
|
|
1943
2608
|
## Keep a generic effect generic
|
|
1944
2609
|
|
|
1945
|
-
In
|
|
2610
|
+
In `$effects` a function loses its first parameter, and TypeScript cannot keep a generic through that step: `<T>(items: T[]) => T` becomes `(items: unknown[]) => unknown`. JavaScript code is not affected. If you want the exact type, use one of two ways.
|
|
1946
2611
|
|
|
1947
2612
|
`effect(fn)`: mark the function where you declare it, then put it in the scope like any other value. No annotation, so a JavaScript editor shows the exact hint too.
|
|
1948
2613
|
|
|
1949
2614
|
```js
|
|
2615
|
+
// pick.js
|
|
1950
2616
|
import { createCommand } from "nanoeffects/actions";
|
|
1951
2617
|
import { effect } from "nanoeffects/scope";
|
|
1952
2618
|
|
|
1953
2619
|
// effect — marks the function, so a scope binds it to the task; the editor keeps its generic
|
|
1954
2620
|
const first = effect((ctx, items) => items[0]);
|
|
1955
2621
|
|
|
1956
|
-
const pick = createCommand("pick", (items, { first }) => first(items), {
|
|
1957
|
-
first, // a plain key of the scope, not an entry of effects
|
|
2622
|
+
export const pick = createCommand("pick", (items, { first }) => first(items), {
|
|
2623
|
+
first, // a plain key of the scope, not an entry of `$effects`
|
|
1958
2624
|
});
|
|
1959
2625
|
```
|
|
1960
2626
|
|
|
1961
2627
|
Call `effect` where you declare the function, as above, and not inline in the scope: TypeScript does not infer the scope from a generic call written there.
|
|
1962
2628
|
|
|
1963
|
-
`Effect<F>` (TypeScript): declare how the effect looks **after** it is bound, and keep it in
|
|
2629
|
+
`Effect<F>` (TypeScript): declare how the effect looks **after** it is bound, and keep it in `$effects`.
|
|
1964
2630
|
|
|
1965
2631
|
```ts
|
|
1966
2632
|
import { createCommand } from "nanoeffects/actions";
|
|
@@ -1975,7 +2641,7 @@ const first: Effect<First> = (ctx, items) => items[0];
|
|
|
1975
2641
|
const pick = createCommand(
|
|
1976
2642
|
"pick",
|
|
1977
2643
|
(id: string, { first }) => first([id]), // string
|
|
1978
|
-
{ effects: { first } },
|
|
2644
|
+
{ $effects: { first } },
|
|
1979
2645
|
);
|
|
1980
2646
|
```
|
|
1981
2647
|
|
|
@@ -2010,8 +2676,298 @@ const getUser = createCommand(
|
|
|
2010
2676
|
- No plugin, no scope: see [Turn scopes on](#turn-scopes-on).
|
|
2011
2677
|
- The fields of the bus are not in the handler of a command made with `createCommand(…, scope)` unless you type `ctx` yourself; the fields of the scope are.
|
|
2012
2678
|
- The string form of `emit` takes no scope.
|
|
2013
|
-
- `effect(fn)` works only as a value of a scope, not as an entry of
|
|
2014
|
-
- A scope on a command, action, job or module is typed without the events of a bus, so `take` in its
|
|
2679
|
+
- `effect(fn)` works only as a value of a scope, not as an entry of `$effects`.
|
|
2680
|
+
- A scope on a command, action, job or module is typed without the events of a bus, so `take` in its `$effects` takes any event name.
|
|
2681
|
+
|
|
2682
|
+
---
|
|
2683
|
+
Source: docs/oop.md
|
|
2684
|
+
|
|
2685
|
+
# Use the bus from a class
|
|
2686
|
+
|
|
2687
|
+
Use this page when **your domain logic lives in classes**: a service, a store or a controller that needs the bus inside its methods. A class made with `extendBus` has `on`, `emit` and the other methods of the bus, and remembers what each instance subscribed to, so `dispose()` ends it all.
|
|
2688
|
+
|
|
2689
|
+
If your logic is plain functions, stay with [modules](actions.md#mount-domains-with-modules): a module is the same idea without a class.
|
|
2690
|
+
|
|
2691
|
+
## The problem without it
|
|
2692
|
+
|
|
2693
|
+
A class that listens to the bus keeps a reference to it, and keeps every unsubscribe function in a list to call when the object goes away. Forget one and the old object keeps handling events. `extendBus` does both for you.
|
|
2694
|
+
|
|
2695
|
+
## Give a class the methods of a bus
|
|
2696
|
+
|
|
2697
|
+
`extendBus(bus, Base)` returns a subclass of `Base`. Its instances have the methods of the bus, and the arguments of `new` go to the constructor of `Base`.
|
|
2698
|
+
|
|
2699
|
+
```js
|
|
2700
|
+
// bus.js
|
|
2701
|
+
import { createBus } from "nanoeffects";
|
|
2702
|
+
|
|
2703
|
+
// createBus — the bus that the services of the app share
|
|
2704
|
+
export const bus = createBus();
|
|
2705
|
+
```
|
|
2706
|
+
|
|
2707
|
+
```js
|
|
2708
|
+
// search.js
|
|
2709
|
+
import { createAction } from "nanoeffects/actions";
|
|
2710
|
+
|
|
2711
|
+
// createAction — the events of the search domain
|
|
2712
|
+
export const queryChanged = createAction("search/query-changed");
|
|
2713
|
+
export const resultsLoaded = createAction("search/results-loaded");
|
|
2714
|
+
```
|
|
2715
|
+
|
|
2716
|
+
```js
|
|
2717
|
+
// search-service.js
|
|
2718
|
+
import { extendBus } from "nanoeffects/oop";
|
|
2719
|
+
import { bus } from "./bus";
|
|
2720
|
+
import { queryChanged, resultsLoaded } from "./search";
|
|
2721
|
+
|
|
2722
|
+
class SearchServiceBase {
|
|
2723
|
+
constructor(api) {
|
|
2724
|
+
this.api = api;
|
|
2725
|
+
// this.on — a subscription of this instance; dispose() removes it
|
|
2726
|
+
this.on(queryChanged, (query, ctx) => this.search(query, ctx)); // an arrow keeps this
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
async search(query, { signal, emit }) {
|
|
2730
|
+
const items = await this.api.search(query, { signal });
|
|
2731
|
+
emit(resultsLoaded(items));
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
// extendBus — a subclass whose instances have on, once, off, emit and dispose
|
|
2736
|
+
export const SearchService = extendBus(bus, SearchServiceBase);
|
|
2737
|
+
```
|
|
2738
|
+
|
|
2739
|
+
```js
|
|
2740
|
+
import { queryChanged } from "./search";
|
|
2741
|
+
import { SearchService } from "./search-service";
|
|
2742
|
+
|
|
2743
|
+
const service = new SearchService(api); // the arguments go to SearchServiceBase
|
|
2744
|
+
service.emit(queryChanged("nano")); // the same bus as bus.emit
|
|
2745
|
+
service.dispose(); // its subscriptions are gone
|
|
2746
|
+
```
|
|
2747
|
+
|
|
2748
|
+
What you get:
|
|
2749
|
+
|
|
2750
|
+
- The class keeps the name of `Base`, and its instances are instances of `Base`.
|
|
2751
|
+
- `on`, `once`, `off` and `emit` act on the bus. They can be destructured: `const { emit } = service`.
|
|
2752
|
+
- The other methods of the bus, such as `emitter` from [`emitterPlugin`](plugins.md), are there too. Only the plugins used **before** `extendBus` count: the names are taken when it runs.
|
|
2753
|
+
- `bus` is the bus, and `Class.bus` is the same one.
|
|
2754
|
+
- `this.on` works in the constructor of `Base`. The methods exist on the prototype of the subclass, which is the prototype of the instance already.
|
|
2755
|
+
|
|
2756
|
+
## Let the class make its bus
|
|
2757
|
+
|
|
2758
|
+
Leave the bus out and the class gets its own, shared by all of its instances.
|
|
2759
|
+
|
|
2760
|
+
```js
|
|
2761
|
+
// notifier.js
|
|
2762
|
+
import { extendBus } from "nanoeffects/oop";
|
|
2763
|
+
|
|
2764
|
+
class NotifierBase {}
|
|
2765
|
+
|
|
2766
|
+
// extendBus without a bus — a new bus for this class; two classes have two buses
|
|
2767
|
+
export const Notifier = extendBus(NotifierBase);
|
|
2768
|
+
|
|
2769
|
+
const first = new Notifier();
|
|
2770
|
+
const second = new Notifier();
|
|
2771
|
+
second.on("notified", (message) => console.log(message));
|
|
2772
|
+
first.emit("notified", "hello"); // the instances share Notifier.bus
|
|
2773
|
+
```
|
|
2774
|
+
|
|
2775
|
+
Pass a bus when you need plugins (`contextPlugin`, `scopePlugin`) or when several classes talk to each other.
|
|
2776
|
+
|
|
2777
|
+
## End the subscriptions with `dispose`
|
|
2778
|
+
|
|
2779
|
+
`dispose()` removes every subscription made through `this.on` and `this.once` of that instance, and nothing else. It is safe to call twice. `instance[Symbol.dispose]()` does the same, so `using service = new SearchService(api)` works where the runtime supports it.
|
|
2780
|
+
|
|
2781
|
+
```js
|
|
2782
|
+
// dispose.js
|
|
2783
|
+
import { createBus } from "nanoeffects";
|
|
2784
|
+
import { extendBus } from "nanoeffects/oop";
|
|
2785
|
+
|
|
2786
|
+
const bus = createBus();
|
|
2787
|
+
const Service = extendBus(bus, class {});
|
|
2788
|
+
|
|
2789
|
+
const service = new Service();
|
|
2790
|
+
const stop = service.on("ping", () => console.log("service heard it"));
|
|
2791
|
+
bus.on("ping", () => console.log("the bus heard it")); // not a subscription of the service
|
|
2792
|
+
|
|
2793
|
+
service.dispose();
|
|
2794
|
+
bus.emit("ping"); // only "the bus heard it"
|
|
2795
|
+
stop(); // the function on returned still works, and does nothing now
|
|
2796
|
+
```
|
|
2797
|
+
|
|
2798
|
+
A subscription that you removed yourself (the function `on` returned, `this.off`, or a `once` that fired) is forgotten, so the instance holds nothing for it.
|
|
2799
|
+
|
|
2800
|
+
## Keep a method of your own
|
|
2801
|
+
|
|
2802
|
+
The class wins any clash of names. If `Base` has `emit`, `dispose`, `on` or `bus`, yours stays and the bus method is not added. A `Base` with its own `dispose` would never end the subscriptions, so call `disposeBus(this)` from there.
|
|
2803
|
+
|
|
2804
|
+
```js
|
|
2805
|
+
// cache.js
|
|
2806
|
+
import { createBus } from "nanoeffects";
|
|
2807
|
+
import { disposeBus, extendBus } from "nanoeffects/oop";
|
|
2808
|
+
|
|
2809
|
+
const bus = createBus();
|
|
2810
|
+
|
|
2811
|
+
class CacheBase {
|
|
2812
|
+
dispose() {
|
|
2813
|
+
this.items.clear(); // your own cleanup
|
|
2814
|
+
disposeBus(this); // disposeBus — ends the subscriptions of this instance
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
items = new Map();
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
export const Cache = extendBus(bus, CacheBase);
|
|
2821
|
+
```
|
|
2822
|
+
|
|
2823
|
+
`disposeBus` does nothing for a value that is not an instance of such a class.
|
|
2824
|
+
|
|
2825
|
+
## Subscribe a method with `@On` and `@Once`
|
|
2826
|
+
|
|
2827
|
+
A decorator turns a method into a handler of an event for each instance, with `this` bound. You write no `this.on` in the constructor, and `dispose()` ends the subscription too.
|
|
2828
|
+
|
|
2829
|
+
The bus and the events are the same as above, in TypeScript:
|
|
2830
|
+
|
|
2831
|
+
```ts
|
|
2832
|
+
// bus.ts
|
|
2833
|
+
import { createBus } from "nanoeffects";
|
|
2834
|
+
|
|
2835
|
+
export const bus = createBus();
|
|
2836
|
+
```
|
|
2837
|
+
|
|
2838
|
+
```ts
|
|
2839
|
+
// search.ts
|
|
2840
|
+
import { createAction } from "nanoeffects/actions";
|
|
2841
|
+
|
|
2842
|
+
export const queryChanged = createAction<string>("search/query-changed");
|
|
2843
|
+
export const resultsLoaded = createAction<string[]>("search/results-loaded");
|
|
2844
|
+
```
|
|
2845
|
+
|
|
2846
|
+
```ts
|
|
2847
|
+
// search-service.ts
|
|
2848
|
+
import { latest } from "nanoeffects/policies";
|
|
2849
|
+
import { extendBus, On, Once } from "nanoeffects/oop";
|
|
2850
|
+
import { bus } from "./bus";
|
|
2851
|
+
import { queryChanged, resultsLoaded } from "./search";
|
|
2852
|
+
|
|
2853
|
+
class SearchServiceBase {
|
|
2854
|
+
// On — this method handles queryChanged for each instance, this is the instance;
|
|
2855
|
+
// latest — each new query aborts the one before it
|
|
2856
|
+
@On(queryChanged, latest)
|
|
2857
|
+
async search(query: string, { signal, emit }: { signal: AbortSignal; emit: (event: unknown) => void }) {
|
|
2858
|
+
const items = await api.search(query, { signal });
|
|
2859
|
+
emit(resultsLoaded(items));
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
// Once — runs for the first event only
|
|
2863
|
+
@Once("search/ready")
|
|
2864
|
+
ready() {
|
|
2865
|
+
console.log("first ready");
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
export const SearchService = extendBus(bus, SearchServiceBase);
|
|
2870
|
+
```
|
|
2871
|
+
|
|
2872
|
+
Things to know:
|
|
2873
|
+
|
|
2874
|
+
- The event is a name, an action creator or a command, as for `bus.on`. For a creator or a command the payload of the method is checked against it.
|
|
2875
|
+
- The second argument, `wrap`, takes the bound handler and returns the one to subscribe: `latest`, or `(handler) => debounce(300, handler)`. A policy needs it: `latest(this.search)` wraps the function without `this`, and the bound handler is what `wrap` gets.
|
|
2876
|
+
- The subscriptions are made right after the constructor of `Base`. An event that is emitted inside that constructor does not reach them.
|
|
2877
|
+
- A subclass inherits them. A method that a subclass overrides is subscribed only if the override is decorated.
|
|
2878
|
+
- On a class that `extendBus` did not make, a decorator does nothing.
|
|
2879
|
+
- Both the decorators of the language and `experimentalDecorators` work. Your compiler has to support the one you use: TypeScript 5 or newer for the first, and the `experimentalDecorators` option for the second.
|
|
2880
|
+
|
|
2881
|
+
## Decorate the class with `@WithBus`
|
|
2882
|
+
|
|
2883
|
+
`@WithBus(bus)` does what `extendBus(bus, Base)` does, as a class decorator. `@WithBus()` makes a bus for the class.
|
|
2884
|
+
|
|
2885
|
+
<!-- docs-check: continue -->
|
|
2886
|
+
|
|
2887
|
+
```ts
|
|
2888
|
+
// search-service.ts
|
|
2889
|
+
import { type BusMethods, On, WithBus } from "nanoeffects/oop";
|
|
2890
|
+
import { bus } from "./bus";
|
|
2891
|
+
import { queryChanged } from "./search";
|
|
2892
|
+
|
|
2893
|
+
// TypeScript does not change the type of a decorated class, so say what it gets;
|
|
2894
|
+
// checker only: at run time WithBus already adds the methods
|
|
2895
|
+
interface SearchService extends BusMethods<typeof bus> {}
|
|
2896
|
+
|
|
2897
|
+
// WithBus — replaces the class by extendBus(bus, SearchService)
|
|
2898
|
+
@WithBus(bus)
|
|
2899
|
+
class SearchService {
|
|
2900
|
+
@On(queryChanged)
|
|
2901
|
+
search(query: string) {
|
|
2902
|
+
console.log(query);
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
export { SearchService };
|
|
2907
|
+
```
|
|
2908
|
+
|
|
2909
|
+
Prefer `extendBus(bus, Base)` when you can: its types come from the call and need no `interface`.
|
|
2910
|
+
|
|
2911
|
+
## Pass a method by hand
|
|
2912
|
+
|
|
2913
|
+
A handler given to `this.on` is called as the bus calls it, with no `this`. A method passed bare loses `this`, as in plain JavaScript:
|
|
2914
|
+
|
|
2915
|
+
```js
|
|
2916
|
+
// handlers.js
|
|
2917
|
+
import { createBus } from "nanoeffects";
|
|
2918
|
+
import { extendBus } from "nanoeffects/oop";
|
|
2919
|
+
|
|
2920
|
+
class BaseWithHandler {
|
|
2921
|
+
name = "handler";
|
|
2922
|
+
|
|
2923
|
+
handle() {
|
|
2924
|
+
console.log(this?.name);
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
listen() {
|
|
2928
|
+
this.on("ping", this.handle); // this is lost: prints undefined
|
|
2929
|
+
this.on("ping", () => this.handle()); // an arrow keeps it
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
export const Listener = extendBus(createBus(), BaseWithHandler);
|
|
2934
|
+
```
|
|
2935
|
+
|
|
2936
|
+
Use an arrow or a decorated method. The library does not bind `this` for you: a policy such as `latest(this.handle)` wraps the function, so binding would work in half of the cases.
|
|
2937
|
+
|
|
2938
|
+
## Type the instances (optional)
|
|
2939
|
+
|
|
2940
|
+
`extendBus` returns a class that takes the arguments of `Base`, and its instances are `Base` plus `BusMethods<typeof bus>`: the function members of the bus (`on`, `emit`, and those of plugins used before) with `dispose` and `bus`. A member of `Base` with the same name keeps its own type.
|
|
2941
|
+
|
|
2942
|
+
```ts
|
|
2943
|
+
// service.ts
|
|
2944
|
+
import { createBus } from "nanoeffects";
|
|
2945
|
+
import { extendBus } from "nanoeffects/oop";
|
|
2946
|
+
|
|
2947
|
+
type Events = { "order/placed": { id: string } };
|
|
2948
|
+
|
|
2949
|
+
// Events — checker only: the payloads of the bus
|
|
2950
|
+
const bus = createBus<Events>();
|
|
2951
|
+
|
|
2952
|
+
class OrderServiceBase {
|
|
2953
|
+
name: string;
|
|
2954
|
+
|
|
2955
|
+
constructor(name: string) {
|
|
2956
|
+
this.name = name;
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
export const OrderService = extendBus(bus, OrderServiceBase);
|
|
2961
|
+
|
|
2962
|
+
const service = new OrderService("main"); // the constructor of OrderServiceBase
|
|
2963
|
+
service.emit("order/placed", { id: "o1" }); // checked against Events
|
|
2964
|
+
```
|
|
2965
|
+
|
|
2966
|
+
## What is not done
|
|
2967
|
+
|
|
2968
|
+
- `this` is not bound in a handler that you pass by hand (see above).
|
|
2969
|
+
- The methods of a plugin used after `extendBus` are not on the class. Use the plugins first, then `extendBus`.
|
|
2970
|
+
- One bus for each instance, mixing the methods into an object that exists, and decorators for fields or parameters are not part of this entry.
|
|
2015
2971
|
|
|
2016
2972
|
---
|
|
2017
2973
|
Source: docs/typescript.md
|
|
@@ -2345,16 +3301,26 @@ The store (`$items`) is not here. Keep it in Effector and update it from the `fo
|
|
|
2345
3301
|
|
|
2346
3302
|
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.
|
|
2347
3303
|
|
|
2348
|
-
```
|
|
2349
|
-
//
|
|
2350
|
-
import { create } from "zustand";
|
|
3304
|
+
```js
|
|
3305
|
+
// bus.js
|
|
2351
3306
|
import { createBus } from "nanoeffects";
|
|
2352
|
-
|
|
3307
|
+
|
|
3308
|
+
export const bus = createBus(); // the bus: only the bus
|
|
3309
|
+
```
|
|
3310
|
+
|
|
3311
|
+
```js
|
|
3312
|
+
// search-store.js
|
|
3313
|
+
import { create } from "zustand";
|
|
2353
3314
|
|
|
2354
3315
|
// create — Zustand: the store replaces $items
|
|
2355
|
-
const useSearchStore = create(() => ({ items: [] }));
|
|
3316
|
+
export const useSearchStore = create(() => ({ items: [] }));
|
|
3317
|
+
```
|
|
2356
3318
|
|
|
2357
|
-
|
|
3319
|
+
```js
|
|
3320
|
+
// search-handlers.js
|
|
3321
|
+
import { debounce, latest } from "nanoeffects/policies";
|
|
3322
|
+
import { bus } from "./bus";
|
|
3323
|
+
import { useSearchStore } from "./search-store";
|
|
2358
3324
|
|
|
2359
3325
|
// latest — a newer query aborts the request in flight; debounce — wait 300 ms of quiet
|
|
2360
3326
|
const search = latest(async (query, { signal }) => {
|
|
@@ -2363,6 +3329,12 @@ const search = latest(async (query, { signal }) => {
|
|
|
2363
3329
|
});
|
|
2364
3330
|
|
|
2365
3331
|
bus.on("query-changed", debounce(300, search));
|
|
3332
|
+
```
|
|
3333
|
+
|
|
3334
|
+
```jsx
|
|
3335
|
+
// Search.jsx
|
|
3336
|
+
import { bus } from "./bus";
|
|
3337
|
+
import { useSearchStore } from "./search-store";
|
|
2366
3338
|
|
|
2367
3339
|
export function Search() {
|
|
2368
3340
|
const items = useSearchStore((state) => state.items); // Zustand: a selector reads items
|
|
@@ -2438,13 +3410,18 @@ formSubmitted.watch((event) => {
|
|
|
2438
3410
|
});
|
|
2439
3411
|
```
|
|
2440
3412
|
|
|
2441
|
-
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:
|
|
3413
|
+
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:
|
|
2442
3414
|
|
|
2443
3415
|
```js
|
|
2444
|
-
//
|
|
3416
|
+
// bus.js
|
|
2445
3417
|
import { createBus } from "nanoeffects";
|
|
2446
3418
|
|
|
2447
|
-
export const bus = createBus(); // the bus:
|
|
3419
|
+
export const bus = createBus(); // the bus: only the bus
|
|
3420
|
+
```
|
|
3421
|
+
|
|
3422
|
+
```js
|
|
3423
|
+
// form-handlers.js
|
|
3424
|
+
import { bus } from "./bus";
|
|
2448
3425
|
|
|
2449
3426
|
// bus.on — the job of sendFormFx: runs when the form is submitted
|
|
2450
3427
|
bus.on("form/submitted", (values) => {
|
|
@@ -2457,7 +3434,8 @@ Then the form. `useState` does the work of `$form`, `fieldUpdate` and `prepend`;
|
|
|
2457
3434
|
```jsx
|
|
2458
3435
|
// App.jsx
|
|
2459
3436
|
import { useState } from "react";
|
|
2460
|
-
import { bus } from "./
|
|
3437
|
+
import { bus } from "./bus"; // the bus of the previous snippet
|
|
3438
|
+
import "./form-handlers"; // registers the handler; the app does this once, when it starts
|
|
2461
3439
|
|
|
2462
3440
|
function Field({ name, type, label, value, onChange }) {
|
|
2463
3441
|
return (
|
|
@@ -2527,16 +3505,27 @@ Retrying a failed network request, and cancelling when the form closes, are in [
|
|
|
2527
3505
|
|
|
2528
3506
|
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.
|
|
2529
3507
|
|
|
2530
|
-
<!-- docs-check: skip - needs effector, which the project does not depend on -->
|
|
2531
3508
|
```js
|
|
2532
|
-
|
|
3509
|
+
// bus.js
|
|
2533
3510
|
import { createBus } from "nanoeffects";
|
|
2534
|
-
import {
|
|
2535
|
-
|
|
3511
|
+
import { emitterPlugin } from "nanoeffects/actions";
|
|
3512
|
+
|
|
3513
|
+
export const bus = createBus().use(emitterPlugin()); // adds bus.emitter
|
|
3514
|
+
```
|
|
3515
|
+
|
|
3516
|
+
```js
|
|
3517
|
+
// search.js
|
|
3518
|
+
import { createAction } from "nanoeffects/actions";
|
|
2536
3519
|
|
|
2537
|
-
const
|
|
3520
|
+
export const queryChanged = createAction("query/changed"); // a plain action creator
|
|
3521
|
+
```
|
|
2538
3522
|
|
|
2539
|
-
|
|
3523
|
+
<!-- docs-check: skip - needs effector, which the project does not depend on -->
|
|
3524
|
+
```js
|
|
3525
|
+
import { createEvent, createStore } from "effector";
|
|
3526
|
+
import { latest } from "nanoeffects/policies";
|
|
3527
|
+
import { bus } from "./bus";
|
|
3528
|
+
import { queryChanged } from "./search";
|
|
2540
3529
|
|
|
2541
3530
|
const found = createEvent(); // Effector: the result comes back as an event
|
|
2542
3531
|
const $items = createStore([]).on(found, (_, items) => items);
|
|
@@ -2659,16 +3648,27 @@ Keep RxJS where a stream is how you think, and let events cross to the bus in ei
|
|
|
2659
3648
|
|
|
2660
3649
|
**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).
|
|
2661
3650
|
|
|
2662
|
-
<!-- docs-check: skip - needs rxjs, which the project does not depend on -->
|
|
2663
3651
|
```js
|
|
2664
|
-
|
|
3652
|
+
// bus.js
|
|
2665
3653
|
import { createBus } from "nanoeffects";
|
|
2666
|
-
import {
|
|
2667
|
-
|
|
3654
|
+
import { emitterPlugin } from "nanoeffects/actions";
|
|
3655
|
+
|
|
3656
|
+
export const bus = createBus().use(emitterPlugin()); // adds bus.emitter
|
|
3657
|
+
```
|
|
3658
|
+
|
|
3659
|
+
```js
|
|
3660
|
+
// filters.js
|
|
3661
|
+
import { createAction } from "nanoeffects/actions";
|
|
2668
3662
|
|
|
2669
|
-
const
|
|
3663
|
+
export const filtersChanged = createAction("filters/changed"); // a plain action creator
|
|
3664
|
+
```
|
|
2670
3665
|
|
|
2671
|
-
|
|
3666
|
+
<!-- docs-check: skip - needs rxjs, which the project does not depend on -->
|
|
3667
|
+
```js
|
|
3668
|
+
import { combineLatest } from "rxjs";
|
|
3669
|
+
import { latest } from "nanoeffects/policies";
|
|
3670
|
+
import { bus } from "./bus";
|
|
3671
|
+
import { filtersChanged } from "./filters";
|
|
2672
3672
|
|
|
2673
3673
|
// RxJS: combine two streams into one value; nanoeffects does not do that
|
|
2674
3674
|
const subscription = combineLatest([query$, category$]).subscribe(
|
|
@@ -2898,7 +3898,7 @@ A `bus.on` outside a module returns the function that unsubscribes: hand it to t
|
|
|
2898
3898
|
|
|
2899
3899
|
## Report leaks with `devPlugin`
|
|
2900
3900
|
|
|
2901
|
-
`bus.use(devPlugin(
|
|
3901
|
+
`bus.use(devPlugin())` reports what is easy to miss while developing:
|
|
2902
3902
|
|
|
2903
3903
|
```js
|
|
2904
3904
|
import { createBus } from "nanoeffects";
|
|
@@ -2906,13 +3906,14 @@ import { modulePlugin } from "nanoeffects/actions";
|
|
|
2906
3906
|
import { devPlugin } from "nanoeffects/dev";
|
|
2907
3907
|
|
|
2908
3908
|
const bus = createBus();
|
|
2909
|
-
|
|
3909
|
+
bus.use(modulePlugin([userModule]));
|
|
2910
3910
|
// devPlugin — warn on piled-up listeners; log module mount/replace/unmount
|
|
2911
|
-
|
|
3911
|
+
// (it finds the app of modulePlugin on the bus, in either order of the two)
|
|
3912
|
+
bus.use(devPlugin());
|
|
2912
3913
|
```
|
|
2913
3914
|
|
|
2914
3915
|
- **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.
|
|
2915
|
-
- **Modules.**
|
|
3916
|
+
- **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.
|
|
2916
3917
|
- **Options.** `leaks: false` and `modules: false` switch a diagnostic off; `maxListeners` sets the limit; `log` receives the messages instead of the console.
|
|
2917
3918
|
- **Disconnect.** `bus.use(devPlugin(...))` returns the function that stops the diagnostics and puts `bus.on` and `app.mount` back.
|
|
2918
3919
|
|
|
@@ -2920,6 +3921,6 @@ It changes no behavior: it wraps `bus.on`, `app.mount`, and `app.unmount` and ad
|
|
|
2920
3921
|
|
|
2921
3922
|
### Keep the import in production
|
|
2922
3923
|
|
|
2923
|
-
`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(
|
|
3924
|
+
`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.
|
|
2924
3925
|
|
|
2925
3926
|
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.
|