nanoeffects 0.7.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.
Files changed (47) hide show
  1. package/dist/{action-creator-Bd4TBZgl.d.ts → action-creator-BrZF6hXc.d.ts} +0 -4
  2. package/dist/actions.d.ts +3 -3
  3. package/dist/actions.js +1 -1
  4. package/dist/{context-plugin-BVNKqkIx.d.ts → context-plugin--HkNN1ha.d.ts} +1 -1
  5. package/dist/dev.d.ts +9 -8
  6. package/dist/dev.js +1 -1
  7. package/dist/effects.d.ts +2 -2
  8. package/dist/effects.js +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/match.d.ts +50 -0
  11. package/dist/match.js +1 -0
  12. package/dist/{module-DZ5ngUrM.d.ts → module-DqKegXHt.d.ts} +43 -18
  13. package/dist/notrace/{action-creator-D-y_-xRS.d.ts → action-creator-DHAEgwuh.d.ts} +0 -4
  14. package/dist/notrace/actions.d.ts +52 -17
  15. package/dist/notrace/actions.js +1 -1
  16. package/dist/notrace/{context-plugin-DDfsaErA.d.ts → context-plugin-BapQkGwQ.d.ts} +1 -1
  17. package/dist/notrace/effects.d.ts +2 -2
  18. package/dist/notrace/effects.js +1 -1
  19. package/dist/notrace/index.d.ts +2 -2
  20. package/dist/notrace/{plugin-CDAngHwg.d.ts → plugin-Cxadz6uU.d.ts} +2 -2
  21. package/dist/notrace/policies.d.ts +1 -1
  22. package/dist/notrace/redux.d.ts +1 -1
  23. package/dist/observable.d.ts +1 -1
  24. package/dist/oop.d.ts +131 -0
  25. package/dist/oop.js +1 -0
  26. package/dist/{plugin-DQNPBcxx.d.ts → plugin-DkXCvsmB.d.ts} +2 -2
  27. package/dist/policies.d.ts +1 -1
  28. package/dist/react.d.ts +1 -1
  29. package/dist/redux.d.ts +1 -1
  30. package/dist/scope.d.ts +5 -6
  31. package/dist/trace.d.ts +1 -1
  32. package/dist/{types-BJ0fzV1p.d.ts → types-KavbvHix.d.ts} +13 -3
  33. package/docs/README.md +1 -0
  34. package/docs/actions.md +79 -35
  35. package/docs/guide.md +142 -18
  36. package/docs/hmr.md +6 -5
  37. package/docs/migrating-from-effector.md +49 -16
  38. package/docs/migrating-from-rxjs.md +17 -6
  39. package/docs/oop.md +286 -0
  40. package/docs/package.md +2 -0
  41. package/docs/plugins.md +227 -51
  42. package/docs/react.md +17 -5
  43. package/docs/scope.md +60 -22
  44. package/docs/stores.md +263 -109
  45. package/llms-full.txt +1152 -267
  46. package/llms.txt +1 -0
  47. package/package.json +20 -2
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
- import { createBus } from "nanoeffects";
272
- import { createCommand, commandsPlugin } from "nanoeffects/actions";
359
+ // user.js
360
+ import { createCommand } from "nanoeffects/actions";
361
+
362
+ export const getUser = createCommand("user/get"); // contract — no handler yet
363
+ ```
273
364
 
274
- const getUser = createCommand("user/get"); // contract — no handler yet
365
+ ```js
366
+ // bus.js
367
+ import { createBus } from "nanoeffects";
368
+ import { commandsPlugin } from "nanoeffects/actions";
275
369
 
276
- const bus = createBus();
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
- import { createBus } from "nanoeffects";
300
- import { createCommand, createModule, modulePlugin } from "nanoeffects/actions";
398
+ // user.js
399
+ import { createCommand } from "nanoeffects/actions";
301
400
 
302
- const getUser = createCommand("user/get"); // contract — no handler yet
303
- const double = createCommand("math/double", (value) => value * 2); // carries its handler
401
+ export const getUser = createCommand("user/get"); // contract — no handler yet
402
+ ```
304
403
 
305
- const bus = createBus();
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` |
@@ -412,55 +538,203 @@ Source: docs/plugins.md
412
538
 
413
539
  # Choose the plugins of a bus
414
540
 
415
- 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 returns, and in what order to use them. Skip it for a small bus: `createBus()` alone is enough.
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.
416
542
 
417
543
  ## Use a plugin
418
544
 
419
- A plugin is a function of the bus. `bus.use(plugin)` calls it with the bus and returns whatever the plugin returns.
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).
420
576
 
421
577
  ```js
422
578
  import { createBus } from "nanoeffects";
579
+ import { delay, effectsPlugin, race } from "nanoeffects/effects";
423
580
 
424
- // your own plugin: a function of the bus that returns what you want back
425
- const countPlugin = (bus) => {
426
- let count = 0;
427
- const unbind = bus.on("*", () => {
428
- count += 1;
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),
429
588
  });
430
- return { count: () => count, stop: unbind };
431
- };
589
+ });
590
+ ```
591
+
592
+ ### Let a test replace a handler: `commandsPlugin`
432
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
433
601
  const bus = createBus();
434
- // use — calls countPlugin(bus) and returns its result
435
- const counter = bus.use(countPlugin);
436
602
 
437
- bus.emit("greet", "Ada");
438
- console.log(counter.count()); // 1
439
- counter.stop();
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
+ }
440
711
  ```
441
712
 
442
- The bus keeps no list of plugins. A plugin that was used cannot be taken out by the bus; it returns its own `stop` or `disconnect` when it needs one.
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";
443
721
 
444
- ## See every plugin
722
+ const bus = createBus();
723
+ bus.use(modulePlugin([userModule]));
445
724
 
446
- | Plugin | Import | It adds | `bus.use` returns | Page |
447
- | ------ | ------ | ------- | ----------------- | ---- |
448
- | `contextPlugin` | `nanoeffects` | Fields on `ctx` of every task | The same bus, typed with the fields | [Context fields](context.md) |
449
- | `effectsPlugin` | `nanoeffects/effects` | `race`, `all`, `take`, `delay` and your own effects on `ctx`, bound to the task | The same bus, typed with the effects | [Effects](effects.md#put-selected-effects-on-ctx) |
450
- | `scopePlugin` | `nanoeffects/scope` | Fields and effects for one command, action, job, module or `emit` | The same bus | [Scopes](scope.md) |
451
- | `commandsPlugin` | `nanoeffects/actions` | The registry of command handlers, `bus.commands` | The registry `{ run, handle }` | [Actions](actions.md#describe-events-commands-and-jobs) |
452
- | `modulePlugin` | `nanoeffects/actions` | Modules that mount and unmount as one unit | The app `{ mount, unmount, isMounted, dispose, commands, createModule }` | [Modules](actions.md#mount-domains-with-modules) |
453
- | `emitterPlugin` | `nanoeffects/actions` | `bus.emitter(creator)`, a callback that emits an action | The same bus, with `emitter` | [Stores](stores.md#emit-from-a-state-manager) |
454
- | `reactPlugin` | `nanoeffects/react` | `bus.useEmitter(event, value)`, a React hook | The same bus, with `useEmitter` | [React and stores](stores.md#react-sharing-a-value-across-components) |
455
- | `devPlugin` | `nanoeffects/dev` | Warnings about listeners that pile up, and messages about modules | A function that disconnects it | [Hot reload](hmr.md#report-leaks-with-devplugin) |
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());
456
728
 
457
- "The same bus" means you can keep writing `bus` and ignore the result. Keep the result of `contextPlugin` and `effectsPlugin` in TypeScript, though: the type of the bus with its new fields is the type they return (`const bus = createBus().use(contextPlugin({ store }))`).
729
+ disconnect(); // stops the diagnostics
730
+ ```
458
731
 
459
732
  ## Put them together
460
733
 
461
- One bus with every plugin that does not need React. The order is explained in the next section.
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.
462
735
 
463
736
  ```js
737
+ // bus.js
464
738
  import { contextPlugin, createBus } from "nanoeffects";
465
739
  import {
466
740
  commandsPlugin,
@@ -469,47 +743,45 @@ import {
469
743
  } from "nanoeffects/actions";
470
744
  import { devPlugin } from "nanoeffects/dev";
471
745
  import { all, effectsPlugin, race } from "nanoeffects/effects";
746
+ import { matchPlugin } from "nanoeffects/match";
472
747
  import { scopePlugin } from "nanoeffects/scope";
473
748
 
474
- const bus = createBus()
749
+ export const bus = createBus()
475
750
  .use(contextPlugin({ store })) // ctx.store on every task
476
751
  .use(effectsPlugin({ race, all })) // ctx.race and ctx.all, bound to the task
477
752
  .use(scopePlugin()) // scopes for commands, actions, jobs, modules and emit
753
+ .use(matchPlugin()) // bus.on takes a function that picks the events
478
754
  .use(emitterPlugin()); // bus.emitter(creator)
479
755
 
480
756
  // commandsPlugin — the registry of command handlers; keep it to bind handlers later
481
- const commands = bus.use(commandsPlugin());
757
+ export const commands = bus.use(commandsPlugin());
482
758
 
483
- // modulePlugin — mounts userModule now; keep app to mount and unmount later
484
- const app = bus.use(modulePlugin([userModule]));
759
+ // modulePlugin — mounts userModule on the first use; keep app to mount and unmount later
760
+ export const app = bus.use(modulePlugin([userModule]));
485
761
 
486
762
  // devPlugin — reports leaks and modules while you develop; keep disconnect to remove it
487
- const disconnect = bus.use(devPlugin({ app }));
763
+ export const disconnect = bus.use(devPlugin());
488
764
  ```
489
765
 
490
- ## Follow the order
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.
491
769
 
492
- Most plugins do not care about each other. These pairs do:
770
+ Two things happen because modules are mounted late:
493
771
 
494
- | Rule | Why |
495
- | ---- | --- |
496
- | `scopePlugin` after `effectsPlugin` | `effectsPlugin` assigns its effects on `ctx`, so one used later would replace an effect with the same name that a scope put there. |
497
- | `scopePlugin` before `modulePlugin` | `modulePlugin` mounts the modules when it is used, and a module with a scope needs the plugin by then. |
498
- | `commandsPlugin` before `modulePlugin`, if you bind handlers yourself | `modulePlugin` uses the registry that exists. Without one it makes its own, which is `app.commands`. |
499
- | `modulePlugin` before `devPlugin({ app })` | `devPlugin` reports the modules of the `app` that `modulePlugin` returned. |
500
- | `emitterPlugin` and `reactPlugin` anywhere | They only call `bus.emit`, which is read when you call them. |
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.
501
774
 
502
- Two more things the checker enforces. A name that is already a context field of the bus is a type error when `contextPlugin` or `effectsPlugin` adds it again. `contextPlugin` does not replace a field that is on `ctx` already, so a scope wins over it in any order.
775
+ Other plugins do not need an order for another reason:
503
776
 
504
- ## Choose by need
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.
505
781
 
506
- - **A store, a client or a logger on every handler.** `contextPlugin`. Or `createBus({ context })` when you do not need the types from the value.
507
- - **`race`, `all` or your own effect without passing `ctx`.** `effectsPlugin`.
508
- - **Fields for one command, or a fake for one test call.** `scopePlugin`.
509
- - **A request with a result, or handlers that a test can replace.** `commandsPlugin`. Add `modulePlugin` when the domain needs a mount and an unmount.
510
- - **A callback for a state manager, such as `$name.listen(...)`.** `emitterPlugin`.
511
- - **Tell the bus when a value of a React component changes.** `reactPlugin`.
512
- - **Find listeners that were never unbound, or see modules reload.** `devPlugin`. In a production build `nanoeffects/dev` is empty, so the call can stay.
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.
513
785
 
514
786
  ## See what is not a plugin
515
787
 
@@ -517,8 +789,38 @@ These connect the bus to something, but are not used with `bus.use`:
517
789
 
518
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).
519
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).
520
793
  - `createObserve(bus)` from `nanoeffects/observable` turns an event into an Observable. See [Observable](observable.md).
521
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
+
522
824
  ---
523
825
  Source: docs/policies.md
524
826
 
@@ -990,14 +1292,28 @@ Recipe without a UI framework: [`examples/modules.ts`](../examples/modules.ts).
990
1292
 
991
1293
  ## Return a value from `emit` with a command
992
1294
 
1295
+ The bus lives in one file and its events in another, so any module can import both without a cycle.
1296
+
993
1297
  ```js
1298
+ // bus.js
994
1299
  import { createBus } from "nanoeffects";
995
- import { createCommand } from "nanoeffects/actions";
996
1300
 
997
- const bus = createBus();
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";
998
1308
 
999
1309
  // createCommand — emit runs the handler and returns its result
1000
- 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
+
1001
1317
  bus.emit(double(21)); // 42
1002
1318
  ```
1003
1319
 
@@ -1006,27 +1322,30 @@ bus.emit(double(21)); // 42
1006
1322
  `nanoeffects/actions` has the pieces to name what a bus does, apart from the code that does it.
1007
1323
 
1008
1324
  ```js
1325
+ // user.js
1009
1326
  import { createAction, createCommand, createJob } from "nanoeffects/actions";
1010
1327
 
1011
1328
  // createAction — event with a payload (like RTK createAction)
1012
- const userLoaded = createAction("user/loaded");
1013
- const loadFailed = createAction("user/failed");
1329
+ export const userLoaded = createAction("user/loaded");
1330
+ export const loadFailed = createAction("user/failed");
1014
1331
 
1015
1332
  // createCommand with a handler — emit returns what the handler returns
1016
- const getUser = createCommand("user/get", (id, { signal }) => {
1333
+ export const getUser = createCommand("user/get", (id, { signal }) => {
1017
1334
  return fetchUser(id, { signal });
1018
1335
  });
1019
1336
 
1020
1337
  // createCommand without a handler — a contract; register the work elsewhere
1021
- const sendMail = createCommand("mail/send");
1338
+ export const sendMail = createCommand("mail/send");
1022
1339
 
1023
1340
  // createJob — command that also emits success / fail actions
1024
- const loadUser = createJob("user/load", fetchUser, {
1341
+ export const loadUser = createJob("user/load", fetchUser, {
1025
1342
  success: userLoaded,
1026
1343
  fail: "user/failed",
1027
1344
  });
1028
1345
  ```
1029
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
+
1030
1349
  ### Actions
1031
1350
 
1032
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")`.
@@ -1047,22 +1366,22 @@ Calling a command builds `{ type, payload }` like `createAction`. `bus.emit(comm
1047
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.
1048
1367
 
1049
1368
  ```js
1369
+ // bus.js
1050
1370
  import { createBus } from "nanoeffects";
1051
- import {
1052
- bindHandler,
1053
- commandsPlugin,
1054
- createCommand,
1055
- } from "nanoeffects/actions";
1056
-
1057
- const sendMail = createCommand("mail/send");
1058
- const getUser = createCommand("user/get");
1371
+ import { bindHandler, commandsPlugin } from "nanoeffects/actions";
1372
+ import { sendMail } from "./user";
1059
1373
 
1060
- const bus = createBus();
1374
+ export const bus = createBus();
1061
1375
 
1062
1376
  // commandsPlugin — registry of handlers for this bus
1063
- const commands = bus.use(
1377
+ export const commands = bus.use(
1064
1378
  commandsPlugin([bindHandler(sendMail, smtpSend)]),
1065
1379
  );
1380
+ ```
1381
+
1382
+ ```js
1383
+ import { commands } from "./bus";
1384
+ import { getUser } from "./user";
1066
1385
 
1067
1386
  // later, e.g. in a test — latest binding wins
1068
1387
  const unbind = commands.handle(getUser, async () => fakeUser);
@@ -1071,7 +1390,7 @@ unbind();
1071
1390
 
1072
1391
  A command with no handler anywhere throws `No handler for command "..."` from `emit` (the call itself only builds the action).
1073
1392
 
1074
- 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 plugin on the same bus replaces it. There is no `commands` option in `createBus`; assign a custom runner with `bus.commands = { run }`.
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 }`.
1075
1394
 
1076
1395
  ### Jobs
1077
1396
 
@@ -1094,18 +1413,20 @@ The registry lives in `nanoeffects/actions`, so a bus without one carries none o
1094
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)).
1095
1414
 
1096
1415
  ```js
1097
- import { createBus } from "nanoeffects";
1098
- import {
1099
- createCommand,
1100
- createModule,
1101
- modulePlugin,
1102
- } from "nanoeffects/actions";
1416
+ // user.js
1417
+ import { createCommand } from "nanoeffects/actions";
1103
1418
 
1104
- const getUser = createCommand("user/get");
1105
- const saveUser = createCommand("user/save", persistUser);
1419
+ export const getUser = createCommand("user/get");
1420
+ export const saveUser = createCommand("user/save", persistUser);
1421
+ ```
1422
+
1423
+ ```js
1424
+ // user-module.js
1425
+ import { createModule } from "nanoeffects/actions";
1426
+ import { getUser, saveUser } from "./user";
1106
1427
 
1107
1428
  // createModule — one domain; unmount undoes everything below
1108
- const userModule = createModule("user", (m) => {
1429
+ export const userModule = createModule("user", (m) => {
1109
1430
  m.handle(getUser, fetchUser);
1110
1431
  m.use(saveUser, authModule);
1111
1432
  m.on("login", (user, ctx) => {
@@ -1115,10 +1436,22 @@ const userModule = createModule("user", (m) => {
1115
1436
  /* background work; ends when the module unmounts */
1116
1437
  });
1117
1438
  });
1439
+ ```
1118
1440
 
1119
- const bus = createBus();
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();
1120
1448
  // modulePlugin — mount modules on the bus
1121
- 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";
1122
1455
 
1123
1456
  const unmount = app.mount(orderModule); // later, e.g. after a lazy import
1124
1457
  app.commands.handle(getUser, fakeUser); // in a test
@@ -1129,6 +1462,7 @@ app.dispose(); // unmounts every module
1129
1462
  - **A module is known by its name.** Mounting a module whose name is taken replaces the mounted one (see [Hot reload](hmr.md)).
1130
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`.
1131
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).
1132
1466
  - Module context = task context plus `handle`, `use`, and `spawn`:
1133
1467
  - `handle` — give a command a handler (contract, or override).
1134
1468
  - `use` — bring sub-modules and commands that carry a handler; the returned function undoes that call.
@@ -1140,17 +1474,29 @@ app.dispose(); // unmounts every module
1140
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`:
1141
1475
 
1142
1476
  ```js
1477
+ // bus.js
1143
1478
  import { createBus } from "nanoeffects";
1144
- import { createCommand } from "nanoeffects/actions";
1145
1479
  import { scopePlugin } from "nanoeffects/scope";
1146
1480
 
1147
1481
  // scopePlugin — makes the scope of a command work on this bus
1148
- 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";
1149
1488
 
1150
1489
  // the third argument — fields of this command only
1151
- const getUser = createCommand("user/get", (id, { api }) => api.getUser(id), {
1152
- api,
1153
- });
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";
1154
1500
 
1155
1501
  // a test replaces them for one call
1156
1502
  const fakeApi = { getUser: async (id) => ({ id, name: "test" }) };
@@ -1181,7 +1527,7 @@ If you do not have a store yet, use `createBus` from the [Guide](guide.md).
1181
1527
  - [React: sharing a value across components](#react-sharing-a-value-across-components)
1182
1528
  - [Callbacks on every change](#callbacks-on-every-change)
1183
1529
  - [Stop watching a store](#stop-watching-a-store)
1184
- - [Give the store to every handler](#give-the-store-to-every-handler)
1530
+ - [Change the store from any handler](#change-the-store-from-any-handler)
1185
1531
 
1186
1532
  ## Wire Redux Toolkit to a bus
1187
1533
 
@@ -1192,23 +1538,40 @@ Redux’s `dispatch` → reducers updates **state**. It does not give you first-
1192
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).
1193
1539
 
1194
1540
  ```js
1195
- import { configureStore, createAction } from "@reduxjs/toolkit";
1541
+ // bus.js
1196
1542
  import { createBus } from "nanoeffects";
1197
- import { latest } from "nanoeffects/policies";
1198
- import { createEffectsMiddleware } from "nanoeffects/redux";
1199
1543
 
1200
- const userRequested = createAction("user/requested"); // RTK
1201
- const userLoaded = createAction("user/loaded");
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
+ ```
1202
1554
 
1203
- const bus = createBus(); // the bus of your app; it can exist before the store
1555
+ ```js
1556
+ // store.js
1557
+ import { configureStore } from "@reduxjs/toolkit";
1558
+ import { createEffectsMiddleware } from "nanoeffects/redux";
1559
+ import { bus } from "./bus";
1204
1560
 
1205
1561
  // createEffectsMiddleware — actions reach the bus after reducers; tasks get ctx.store
1206
1562
  const effects = createEffectsMiddleware(bus);
1207
1563
 
1208
- const store = configureStore({
1564
+ export const store = configureStore({
1209
1565
  reducer,
1210
1566
  middleware: (getDefault) => getDefault().concat(effects),
1211
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";
1212
1575
 
1213
1576
  // latest — a newer run aborts the older one
1214
1577
  bus.on(
@@ -1259,28 +1622,44 @@ const effects = createEffectsMiddleware(bus);
1259
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:
1260
1623
 
1261
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.
1262
- - **The event changes the store.** The handler reports its result as an event. A listener of the bus writes it into the store.
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.
1263
1626
 
1264
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`.
1265
1628
 
1266
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.
1267
1630
 
1268
- The bus is the same for every store below. Create it once; each subsection imports it.
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.
1269
1637
 
1270
1638
  ```js
1271
- // search-bus.js
1272
- import { createBus } from "nanoeffects";
1273
- import { createAction, emitterPlugin } from "nanoeffects/actions";
1274
- import { debounce, latest } from "nanoeffects/policies";
1639
+ // bus.js
1640
+ import { contextPlugin, createBus } from "nanoeffects";
1641
+ import { emitterPlugin } from "nanoeffects/actions";
1642
+ import { store } from "./store";
1275
1643
 
1276
- export const bus = createBus().use(emitterPlugin()); // adds bus.emitter; returns the same bus
1644
+ export const bus = createBus()
1645
+ .use(emitterPlugin()) // adds bus.emitter
1646
+ .use(contextPlugin({ store })); // ctx.store on every task
1647
+ ```
1648
+
1649
+ ```js
1650
+ // search.js
1651
+ import { createAction } from "nanoeffects/actions";
1277
1652
 
1278
1653
  // createAction — plain action creators; calling one only builds { type, payload }
1279
1654
  export const queryChanged = createAction("search/queryChanged");
1280
1655
  export const searchLoaded = createAction("search/loaded");
1656
+ ```
1281
1657
 
1282
- // bus.emitter: a callback (query) => bus.emit(queryChanged(query))
1283
- export const emitQueryChanged = bus.emitter(queryChanged);
1658
+ ```js
1659
+ // search-handlers.js
1660
+ import { debounce, latest } from "nanoeffects/policies";
1661
+ import { bus } from "./bus";
1662
+ import { queryChanged, searchLoaded } from "./search";
1284
1663
 
1285
1664
  // debounce — wait 300 ms of quiet; latest — abort the search of the previous query
1286
1665
  bus.on(
@@ -1294,141 +1673,199 @@ bus.on(
1294
1673
  }),
1295
1674
  ),
1296
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));
1297
1691
  ```
1298
1692
 
1299
1693
  Write `results` and never the field you watch: a handler that changed `query` would start itself again. Find your store below.
1300
1694
 
1301
1695
  ### Nanostores
1302
1696
 
1303
- `listen` calls back with the new value. `subscribe` would also call at once, with the current value, so use `listen`.
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`.
1304
1698
 
1305
1699
  ```js
1306
- import { atom } from "nanostores";
1307
- import { bus, emitQueryChanged, searchLoaded } from "./search-bus";
1700
+ // store.js
1701
+ import { action, atom } from "nanostores";
1308
1702
 
1309
1703
  const $query = atom("");
1310
1704
  const $results = atom([]);
1311
1705
 
1312
- // store → bus: listen calls back with the new query on every change
1313
- const stopQuery = $query.listen(emitQueryChanged);
1314
-
1315
- // bus → store: bus.on returns the function that removes this listener
1316
- const stopResults = bus.on(searchLoaded, (items) => {
1317
- $results.set(items);
1318
- });
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
+ };
1319
1713
  ```
1320
1714
 
1321
1715
  ### MobX
1322
1716
 
1323
- `reaction` watches an expression and calls back with its value. Change observables inside `runInAction`.
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 -->
1324
1720
 
1325
1721
  ```js
1326
- import { observable, reaction, runInAction } from "mobx";
1327
- import { bus, emitQueryChanged, searchLoaded } from "./search-bus";
1722
+ // store.js
1723
+ import { makeAutoObservable, reaction } from "mobx";
1328
1724
 
1329
- const search = observable({ query: "", results: [] });
1725
+ class SearchStore {
1726
+ query = "";
1727
+ results = [];
1330
1728
 
1331
- // store → bus: reaction watches the expression and calls back with its value
1332
- const stopQuery = reaction(() => search.query, emitQueryChanged);
1729
+ constructor() {
1730
+ makeAutoObservable(this); // the methods below become actions
1731
+ }
1333
1732
 
1334
- // bus → store: bus.on returns the function that removes this listener
1335
- const stopResults = bus.on(searchLoaded, (items) => {
1336
- runInAction(() => {
1337
- search.results = items;
1338
- });
1339
- });
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();
1340
1748
  ```
1341
1749
 
1342
1750
  ### Valtio
1343
1751
 
1344
- `subscribeKey` watches one field of a proxy state. Assign to the proxy to change it.
1752
+ `subscribeKey` watches one field of a proxy state. A mutation assigns to the proxy.
1753
+
1754
+ <!-- docs-check: fork -->
1345
1755
 
1346
1756
  ```js
1757
+ // store.js
1347
1758
  import { proxy } from "valtio";
1348
1759
  import { subscribeKey } from "valtio/utils";
1349
- import { bus, emitQueryChanged, searchLoaded } from "./search-bus";
1350
1760
 
1351
1761
  const state = proxy({ query: "", results: [] });
1352
1762
 
1353
- // store → bus: subscribeKey watches one field
1354
- const stopQuery = subscribeKey(state, "query", emitQueryChanged);
1355
-
1356
- // bus → store: bus.on returns the function that removes this listener
1357
- const stopResults = bus.on(searchLoaded, (items) => {
1358
- state.results = items;
1359
- });
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
+ };
1360
1773
  ```
1361
1774
 
1362
1775
  ### Jotai
1363
1776
 
1364
- `sub` gives no value, so read it yourself. Write with `store.set`.
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 -->
1365
1780
 
1366
1781
  ```js
1782
+ // store.js
1367
1783
  import { atom, createStore } from "jotai";
1368
- import { bus, emitQueryChanged, searchLoaded } from "./search-bus";
1369
1784
 
1370
1785
  const queryAtom = atom("");
1371
1786
  const resultsAtom = atom([]);
1372
- const store = createStore();
1373
-
1374
- // store → bus: sub calls back without a value; store.get reads it
1375
- const stopQuery = store.sub(queryAtom, () => emitQueryChanged(store.get(queryAtom)));
1376
-
1377
- // bus → store: bus.on returns the function that removes this listener
1378
- const stopResults = bus.on(searchLoaded, (items) => {
1379
- store.set(resultsAtom, items);
1380
- });
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
+ };
1381
1800
  ```
1382
1801
 
1383
1802
  ### Zustand
1384
1803
 
1385
- 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 -->
1386
1807
 
1387
1808
  ```js
1809
+ // store.js
1388
1810
  import { create } from "zustand";
1389
1811
  import { subscribeWithSelector } from "zustand/middleware";
1390
- import { bus, emitQueryChanged, searchLoaded } from "./search-bus";
1391
1812
 
1392
- const useSearchStore = create(
1393
- subscribeWithSelector(() => ({ query: "", results: [] })),
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
+ })),
1394
1821
  );
1395
1822
 
1396
- // store → bus: subscribe with a selector calls back with state.query only when it changes
1397
- const stopQuery = useSearchStore.subscribe((state) => state.query, emitQueryChanged);
1398
-
1399
- // bus → store: bus.on returns the function that removes this listener
1400
- const stopResults = bus.on(searchLoaded, (items) => {
1401
- useSearchStore.setState({ results: items });
1402
- });
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
+ };
1403
1829
  ```
1404
1830
 
1405
1831
  ### Pinia
1406
1832
 
1407
- 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. Assign to the store to change it.
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 -->
1408
1836
 
1409
1837
  ```js
1410
- import { watch } from "vue";
1838
+ // store.js
1411
1839
  import { defineStore } from "pinia";
1412
- import { bus, emitQueryChanged, searchLoaded } from "./search-bus";
1840
+ import { watch } from "vue";
1413
1841
 
1414
- // defineStore — Pinia: the state of one store
1842
+ // defineStore — Pinia: the state and the actions of one store
1415
1843
  const useSearch = defineStore("search", {
1416
1844
  state: () => ({ query: "", results: [] }),
1845
+ actions: {
1846
+ setQuery(query) {
1847
+ this.query = query;
1848
+ },
1849
+ setResults(items) {
1850
+ this.results = items;
1851
+ },
1852
+ },
1417
1853
  });
1418
1854
 
1419
1855
  const search = useSearch(); // call it after the app has installed Pinia
1420
1856
 
1421
- // store → bus: watch calls back with the new query when search.query changes
1422
- const stopQuery = watch(() => search.query, emitQueryChanged);
1423
-
1424
- // bus → store: bus.on returns the function that removes this listener
1425
- const stopResults = bus.on(searchLoaded, (items) => {
1426
- search.results = items;
1427
- });
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
+ };
1428
1863
  ```
1429
1864
 
1430
1865
  ### React: sharing a value across components
1431
1866
 
1867
+ <!-- docs-check: new -->
1868
+
1432
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.
1433
1870
 
1434
1871
  | The value lives in | Use when |
@@ -1441,20 +1878,30 @@ A value of your UI (a query, a filter, a name) often has two jobs. Several compo
1441
1878
 
1442
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.
1443
1880
 
1444
- 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:
1445
1882
 
1446
1883
  ```js
1447
- // react-search-bus.js
1884
+ // bus.js
1448
1885
  import { createBus } from "nanoeffects";
1449
- import { createAction } from "nanoeffects/actions";
1450
- import { debounce, latest } from "nanoeffects/policies";
1451
1886
  import { reactPlugin } from "nanoeffects/react";
1452
1887
 
1453
1888
  export const bus = createBus().use(reactPlugin()); // the bus; reactPlugin adds bus.useEmitter
1454
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";
1455
1895
 
1456
1896
  export const queryChanged = createAction("query/changed");
1457
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";
1458
1905
 
1459
1906
  // debounce — wait 300 ms of quiet; latest — a newer query aborts the older request
1460
1907
  bus.on(
@@ -1474,7 +1921,8 @@ Then the provider. It holds the state, calls `useEmitter` once, and listens for
1474
1921
  ```jsx
1475
1922
  // SearchProvider.jsx
1476
1923
  import { createContext, useContext, useEffect, useState } from "react";
1477
- import { bus, queryChanged, searchLoaded, useEmitter } from "./react-search-bus";
1924
+ import { bus, useEmitter } from "./bus";
1925
+ import { queryChanged, searchLoaded } from "./search";
1478
1926
 
1479
1927
  const SearchContext = createContext(null);
1480
1928
 
@@ -1531,6 +1979,8 @@ Two limits to know:
1531
1979
 
1532
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`:
1533
1981
 
1982
+ <!-- docs-check: new -->
1983
+
1534
1984
  ```jsx
1535
1985
  // value-store.js
1536
1986
  import { useSyncExternalStore } from "react";
@@ -1560,19 +2010,36 @@ export function createValue(initial) {
1560
2010
  export const useValue = (store) => useSyncExternalStore(store.subscribe, store.get);
1561
2011
  ```
1562
2012
 
1563
- ```jsx
1564
- // search-value.jsx
1565
- import { createBus, contextPlugin } from "nanoeffects";
1566
- import { createAction, emitterPlugin } from "nanoeffects/actions";
1567
- import { createValue, useValue } from "./value-store";
2013
+ ```js
2014
+ // query.js
2015
+ import { createValue } from "./value-store";
2016
+
2017
+ export const query = createValue("");
2018
+ ```
1568
2019
 
1569
- const query = createValue("");
2020
+ ```js
2021
+ // bus.js
2022
+ import { contextPlugin, createBus } from "nanoeffects";
2023
+ import { emitterPlugin } from "nanoeffects/actions";
2024
+ import { query } from "./query";
1570
2025
 
1571
2026
  // emitterPlugin adds bus.emitter; contextPlugin puts `query` on every task as ctx.query
1572
- const bus = createBus().use(emitterPlugin()).use(contextPlugin({ query }));
2027
+ export const bus = createBus().use(emitterPlugin()).use(contextPlugin({ query }));
2028
+ ```
2029
+
2030
+ ```js
2031
+ // search.js
2032
+ import { createAction } from "nanoeffects/actions";
1573
2033
 
1574
- const queryChanged = createAction("query/changed");
1575
- const refreshed = createAction("search/refreshed");
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";
1576
2043
 
1577
2044
  // store → bus: bus.emitter turns the action into a callback; one place, any number of setters
1578
2045
  const emitQueryChanged = bus.emitter(queryChanged);
@@ -1583,6 +2050,12 @@ bus.on(refreshed, async (_payload, ctx) => {
1583
2050
  const items = await api.search(ctx.query.get(), { signal: ctx.signal });
1584
2051
  console.log(items.length);
1585
2052
  });
2053
+ ```
2054
+
2055
+ ```jsx
2056
+ // SearchBox.jsx
2057
+ import { query } from "./query";
2058
+ import { useValue } from "./value-store";
1586
2059
 
1587
2060
  // any number of components read and write the same value
1588
2061
  export function SearchBox() {
@@ -1595,16 +2068,31 @@ export function SearchBox() {
1595
2068
 
1596
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:
1597
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
+
1598
2088
  ```jsx
1599
- // search-atom.jsx
2089
+ // SearchBox.jsx
1600
2090
  import { atom } from "nanostores";
1601
2091
  import { useStore } from "@nanostores/react";
1602
- import { createBus } from "nanoeffects";
1603
- import { createAction, emitterPlugin } from "nanoeffects/actions";
2092
+ import { bus } from "./bus";
2093
+ import { queryChanged } from "./search";
1604
2094
 
1605
- const bus = createBus().use(emitterPlugin()); // adds bus.emitter
1606
2095
  const $query = atom("");
1607
- const queryChanged = createAction("query/changed");
1608
2096
 
1609
2097
  // listen, not subscribe: listen skips the call with the current value at start
1610
2098
  $query.listen(bus.emitter(queryChanged));
@@ -1619,16 +2107,28 @@ export function SearchBox() {
1619
2107
 
1620
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.
1621
2109
 
1622
- ```jsx
1623
- // search-reducer.jsx
1624
- import { useReducer } from "react";
2110
+ <!-- docs-check: new -->
2111
+
2112
+ ```js
2113
+ // bus.js
1625
2114
  import { createBus } from "nanoeffects";
2115
+
2116
+ export const bus = createBus(); // the bus: created once
2117
+ ```
2118
+
2119
+ ```js
2120
+ // search.js
1626
2121
  import { createAction } from "nanoeffects/actions";
1627
2122
 
1628
- const bus = createBus(); // the bus: created once
2123
+ export const queryChanged = createAction("query/changed");
2124
+ export const queryCleared = createAction("query/cleared");
2125
+ ```
1629
2126
 
1630
- const queryChanged = createAction("query/changed");
1631
- const queryCleared = createAction("query/cleared");
2127
+ ```jsx
2128
+ // Search.jsx
2129
+ import { useReducer } from "react";
2130
+ import { bus } from "./bus";
2131
+ import { queryChanged, queryCleared } from "./search";
1632
2132
 
1633
2133
  function reducer(state, action) {
1634
2134
  if (queryChanged.match(action)) {
@@ -1668,11 +2168,11 @@ The stores above call back only when the value changed, so the emitter does not
1668
2168
 
1669
2169
  ### Stop watching a store
1670
2170
 
1671
- Stop both directions with what the calls returned: `stopQuery` from the store and `stopResults` from `bus.on`. Call them yourself, or hand them over: `onAbort(ctx, stopQuery)` inside a task, or `m.signal.addEventListener("abort", stopQuery)` in a module.
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.
1672
2172
 
1673
- ### Give the store to every handler
2173
+ ### Change the store from any handler
1674
2174
 
1675
- Instead of a listener per store, give the store to all tasks with `createBus({ context: { store } })` ([Context fields](context.md)) and change it from any handler (`setState`, a MobX action, a Pinia store). The same rule holds: write the field you do not watch. For a MobX `flow`, `onAbort(ctx, () => promise.cancel())` cancels it with the task.
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.
1676
2176
 
1677
2177
  ---
1678
2178
  Source: docs/react.md
@@ -1695,19 +2195,26 @@ Each has a policy or an effect: `latest`, `leading`, `retry`. They live in one p
1695
2195
 
1696
2196
  ## Wire the bus once
1697
2197
 
1698
- Put the bus and its handlers in their own file. Create it once, outside any component: a bus created in the body of a component would be a new bus, without subscribers, on every render.
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.
1699
2199
 
1700
2200
  ```js
1701
- // registration.js
2201
+ // bus.js
1702
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
1703
2211
  import { retry } from "nanoeffects/effects";
1704
2212
  import { debounce, latest, leading } from "nanoeffects/policies";
2213
+ import { bus } from "./bus";
1705
2214
 
1706
2215
  // fetch rejects with a TypeError when the network fails; an HTTP error is a plain Error
1707
2216
  const isNetworkError = (error) => error instanceof TypeError;
1708
2217
 
1709
- export const bus = createBus(); // the bus: created once, at the top of the module
1710
-
1711
2218
  // debounce — wait 300 ms of quiet; latest — abort the check of the previous name
1712
2219
  bus.on(
1713
2220
  "username/typed",
@@ -1745,12 +2252,15 @@ The handlers know nothing about React. A test can emit `username/typed` and read
1745
2252
 
1746
2253
  ## Connect the form
1747
2254
 
2255
+ <!-- docs-check: continue -->
2256
+
1748
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.
1749
2258
 
1750
2259
  ```jsx
1751
2260
  // SignUpForm.jsx
1752
2261
  import { useEffect, useState } from "react";
1753
- import { bus } from "./registration"; // the bus of the previous snippet
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
1754
2264
 
1755
2265
  export function SignUpForm() {
1756
2266
  const [name, setName] = useState("");
@@ -1813,6 +2323,8 @@ In development, React StrictMode runs the effect twice (subscribe, unsubscribe,
1813
2323
 
1814
2324
  ## Stop running work when the form closes
1815
2325
 
2326
+ <!-- docs-check: continue -->
2327
+
1816
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`:
1817
2329
 
1818
2330
  ```js
@@ -1913,27 +2425,29 @@ You give a scope to the thing that uses it. A call can replace it. Nothing else
1913
2425
  Scopes are a plugin, so a bus that does not use them pays nothing.
1914
2426
 
1915
2427
  ```js
2428
+ // bus.js
1916
2429
  import { createBus } from "nanoeffects";
1917
2430
  import { scopePlugin } from "nanoeffects/scope";
1918
2431
 
1919
2432
  // scopePlugin — makes the scopes below work on this bus; returns the same bus
1920
- const bus = createBus().use(scopePlugin());
2433
+ export const bus = createBus().use(scopePlugin());
1921
2434
  ```
1922
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
+
1923
2438
  - Without `scopePlugin` a scope is **ignored**: no error and no warning. If a field is missing from `ctx`, check this first.
1924
- - Use it **after** `effectsPlugin` (an effect it adds later would be assigned over an effect of a scope with the same name) and **before** `modulePlugin` (it mounts the modules when it is used). `contextPlugin` and `createBus({ context })` can come in any order: a scope wins over them.
2439
+ - Use it in any order with the other plugins: a scope wins over `effectsPlugin`, `contextPlugin` and `createBus({ context })`.
1925
2440
 
1926
2441
  ## Give a command its own fields
1927
2442
 
2443
+ <!-- docs-check: continue -->
2444
+
1928
2445
  ```js
1929
- import { createBus } from "nanoeffects";
2446
+ // user.js
1930
2447
  import { createCommand } from "nanoeffects/actions";
1931
- import { scopePlugin } from "nanoeffects/scope";
1932
-
1933
- const bus = createBus().use(scopePlugin());
1934
2448
 
1935
2449
  // createCommand — third argument is the scope of this command
1936
- const getUser = createCommand(
2450
+ export const getUser = createCommand(
1937
2451
  "user/get",
1938
2452
  (id, { api, cache }) => {
1939
2453
  // api and cache are on ctx for this command only
@@ -1941,6 +2455,11 @@ const getUser = createCommand(
1941
2455
  },
1942
2456
  { api, cache: new Map() },
1943
2457
  );
2458
+ ```
2459
+
2460
+ ```js
2461
+ import { bus } from "./bus";
2462
+ import { getUser } from "./user";
1944
2463
 
1945
2464
  const user = await bus.emit(getUser("1")); // the handler has api and cache
1946
2465
  ```
@@ -1952,18 +2471,15 @@ The listeners of `"user/get"` get the same fields when the command is emitted, b
1952
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.
1953
2472
 
1954
2473
  ```js
1955
- import { createBus } from "nanoeffects";
2474
+ // feed.js
1956
2475
  import { createCommand } from "nanoeffects/actions";
1957
2476
  import { delay, race } from "nanoeffects/effects";
1958
- import { scopePlugin } from "nanoeffects/scope";
1959
-
1960
- const bus = createBus().use(scopePlugin());
1961
2477
 
1962
2478
  // your own effect: first parameter is the task (only its signal is used)
1963
2479
  const fetchJson = (ctx, url) =>
1964
2480
  fetch(url, { signal: ctx.signal }).then((response) => response.json());
1965
2481
 
1966
- const loadFeed = createCommand(
2482
+ export const loadFeed = createCommand(
1967
2483
  "feed/load",
1968
2484
  (url, { fetchJson, race, delay }) =>
1969
2485
  // race, delay, fetchJson — bound to this task: no ctx argument
@@ -1974,6 +2490,19 @@ const loadFeed = createCommand(
1974
2490
  // $effects — built-in and your own functions share one object
1975
2491
  { $effects: { fetchJson, race, delay } },
1976
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";
1977
2506
 
1978
2507
  const result = await bus.emit(loadFeed("/feed"));
1979
2508
  ```
@@ -1987,14 +2516,27 @@ const result = await bus.emit(loadFeed("/feed"));
1987
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.
1988
2517
 
1989
2518
  ```js
2519
+ // bus.js
1990
2520
  import { createBus } from "nanoeffects";
1991
- import { createCommand } from "nanoeffects/actions";
1992
2521
  import { scopePlugin } from "nanoeffects/scope";
1993
2522
 
1994
- const bus = createBus().use(scopePlugin());
1995
- const getUser = createCommand("user/get", (id, { api }) => api.getUser(id), {
1996
- api,
1997
- });
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";
1998
2540
 
1999
2541
  // the real api for this call
2000
2542
  await bus.emit(getUser("1"));
@@ -2014,23 +2556,30 @@ Rules for the second argument:
2014
2556
  ## Give an action, a job or a module its own fields
2015
2557
 
2016
2558
  ```js
2017
- import { createAction, createJob, createModule } from "nanoeffects/actions";
2559
+ // user.js
2560
+ import { createAction, createJob } from "nanoeffects/actions";
2018
2561
 
2019
2562
  // createAction — the listeners of this action's calls get the fields
2020
- const userLoaded = createAction("user/loaded", { store });
2563
+ export const userLoaded = createAction("user/loaded", { store });
2021
2564
 
2022
2565
  // createJob — the scope is the fourth argument, after the outcomes;
2023
2566
  // the handler and the listeners of the outcome get its fields
2024
- const loadUser = createJob(
2567
+ export const loadUser = createJob(
2025
2568
  "user/load",
2026
2569
  fetchUser,
2027
2570
  { success: userLoaded, fail: "user/failed" },
2028
2571
  { logger },
2029
2572
  );
2573
+ ```
2574
+
2575
+ ```js
2576
+ // audit-module.js
2577
+ import { createModule } from "nanoeffects/actions";
2578
+ import { userLoaded } from "./user";
2030
2579
 
2031
2580
  // createModule — third argument; everything the module owns gets the fields:
2032
2581
  // m.on listeners, m.handle handlers, m.spawn tasks
2033
- const auditModule = createModule(
2582
+ export const auditModule = createModule(
2034
2583
  "audit",
2035
2584
  (m) => {
2036
2585
  m.on(userLoaded, (user, { logger }) => logger.info(user.id));
@@ -2063,13 +2612,14 @@ In `$effects` a function loses its first parameter, and TypeScript cannot keep a
2063
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.
2064
2613
 
2065
2614
  ```js
2615
+ // pick.js
2066
2616
  import { createCommand } from "nanoeffects/actions";
2067
2617
  import { effect } from "nanoeffects/scope";
2068
2618
 
2069
2619
  // effect — marks the function, so a scope binds it to the task; the editor keeps its generic
2070
2620
  const first = effect((ctx, items) => items[0]);
2071
2621
 
2072
- const pick = createCommand("pick", (items, { first }) => first(items), {
2622
+ export const pick = createCommand("pick", (items, { first }) => first(items), {
2073
2623
  first, // a plain key of the scope, not an entry of `$effects`
2074
2624
  });
2075
2625
  ```
@@ -2129,6 +2679,296 @@ const getUser = createCommand(
2129
2679
  - `effect(fn)` works only as a value of a scope, not as an entry of `$effects`.
2130
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.
2131
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.
2971
+
2132
2972
  ---
2133
2973
  Source: docs/typescript.md
2134
2974
 
@@ -2461,16 +3301,26 @@ The store (`$items`) is not here. Keep it in Effector and update it from the `fo
2461
3301
 
2462
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.
2463
3303
 
2464
- ```jsx
2465
- // search-zustand.jsx
2466
- import { create } from "zustand";
3304
+ ```js
3305
+ // bus.js
2467
3306
  import { createBus } from "nanoeffects";
2468
- import { debounce, latest } from "nanoeffects/policies";
3307
+
3308
+ export const bus = createBus(); // the bus: only the bus
3309
+ ```
3310
+
3311
+ ```js
3312
+ // search-store.js
3313
+ import { create } from "zustand";
2469
3314
 
2470
3315
  // create — Zustand: the store replaces $items
2471
- const useSearchStore = create(() => ({ items: [] }));
3316
+ export const useSearchStore = create(() => ({ items: [] }));
3317
+ ```
2472
3318
 
2473
- const bus = createBus(); // the bus: events and handlers
3319
+ ```js
3320
+ // search-handlers.js
3321
+ import { debounce, latest } from "nanoeffects/policies";
3322
+ import { bus } from "./bus";
3323
+ import { useSearchStore } from "./search-store";
2474
3324
 
2475
3325
  // latest — a newer query aborts the request in flight; debounce — wait 300 ms of quiet
2476
3326
  const search = latest(async (query, { signal }) => {
@@ -2479,6 +3329,12 @@ const search = latest(async (query, { signal }) => {
2479
3329
  });
2480
3330
 
2481
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";
2482
3338
 
2483
3339
  export function Search() {
2484
3340
  const items = useSearchStore((state) => state.items); // Zustand: a selector reads items
@@ -2554,13 +3410,18 @@ formSubmitted.watch((event) => {
2554
3410
  });
2555
3411
  ```
2556
3412
 
2557
- 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:
2558
3414
 
2559
3415
  ```js
2560
- // form-bus.js
3416
+ // bus.js
2561
3417
  import { createBus } from "nanoeffects";
2562
3418
 
2563
- export const bus = createBus(); // the bus: events and their handlers
3419
+ export const bus = createBus(); // the bus: only the bus
3420
+ ```
3421
+
3422
+ ```js
3423
+ // form-handlers.js
3424
+ import { bus } from "./bus";
2564
3425
 
2565
3426
  // bus.on — the job of sendFormFx: runs when the form is submitted
2566
3427
  bus.on("form/submitted", (values) => {
@@ -2573,7 +3434,8 @@ Then the form. `useState` does the work of `$form`, `fieldUpdate` and `prepend`;
2573
3434
  ```jsx
2574
3435
  // App.jsx
2575
3436
  import { useState } from "react";
2576
- import { bus } from "./form-bus"; // the bus of the previous snippet
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
2577
3439
 
2578
3440
  function Field({ name, type, label, value, onChange }) {
2579
3441
  return (
@@ -2643,16 +3505,27 @@ Retrying a failed network request, and cancelling when the form closes, are in [
2643
3505
 
2644
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.
2645
3507
 
2646
- <!-- docs-check: skip - needs effector, which the project does not depend on -->
2647
3508
  ```js
2648
- import { createEvent, createStore } from "effector";
3509
+ // bus.js
2649
3510
  import { createBus } from "nanoeffects";
2650
- import { createAction, emitterPlugin } from "nanoeffects/actions";
2651
- import { latest } from "nanoeffects/policies";
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";
2652
3519
 
2653
- const bus = createBus().use(emitterPlugin()); // adds bus.emitter
3520
+ export const queryChanged = createAction("query/changed"); // a plain action creator
3521
+ ```
2654
3522
 
2655
- const queryChanged = createAction("query/changed"); // a plain action creator
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";
2656
3529
 
2657
3530
  const found = createEvent(); // Effector: the result comes back as an event
2658
3531
  const $items = createStore([]).on(found, (_, items) => items);
@@ -2775,16 +3648,27 @@ Keep RxJS where a stream is how you think, and let events cross to the bus in ei
2775
3648
 
2776
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).
2777
3650
 
2778
- <!-- docs-check: skip - needs rxjs, which the project does not depend on -->
2779
3651
  ```js
2780
- import { combineLatest } from "rxjs";
3652
+ // bus.js
2781
3653
  import { createBus } from "nanoeffects";
2782
- import { createAction, emitterPlugin } from "nanoeffects/actions";
2783
- import { latest } from "nanoeffects/policies";
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";
2784
3662
 
2785
- const bus = createBus().use(emitterPlugin()); // adds bus.emitter
3663
+ export const filtersChanged = createAction("filters/changed"); // a plain action creator
3664
+ ```
2786
3665
 
2787
- const filtersChanged = createAction("filters/changed"); // a plain action creator
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";
2788
3672
 
2789
3673
  // RxJS: combine two streams into one value; nanoeffects does not do that
2790
3674
  const subscription = combineLatest([query$, category$]).subscribe(
@@ -3014,7 +3898,7 @@ A `bus.on` outside a module returns the function that unsubscribes: hand it to t
3014
3898
 
3015
3899
  ## Report leaks with `devPlugin`
3016
3900
 
3017
- `bus.use(devPlugin({ app }))` reports what is easy to miss while developing:
3901
+ `bus.use(devPlugin())` reports what is easy to miss while developing:
3018
3902
 
3019
3903
  ```js
3020
3904
  import { createBus } from "nanoeffects";
@@ -3022,13 +3906,14 @@ import { modulePlugin } from "nanoeffects/actions";
3022
3906
  import { devPlugin } from "nanoeffects/dev";
3023
3907
 
3024
3908
  const bus = createBus();
3025
- const app = bus.use(modulePlugin([userModule]));
3909
+ bus.use(modulePlugin([userModule]));
3026
3910
  // devPlugin — warn on piled-up listeners; log module mount/replace/unmount
3027
- bus.use(devPlugin({ app }));
3911
+ // (it finds the app of modulePlugin on the bus, in either order of the two)
3912
+ bus.use(devPlugin());
3028
3913
  ```
3029
3914
 
3030
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.
3031
- - **Modules.** With `app` 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.
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.
3032
3917
  - **Options.** `leaks: false` and `modules: false` switch a diagnostic off; `maxListeners` sets the limit; `log` receives the messages instead of the console.
3033
3918
  - **Disconnect.** `bus.use(devPlugin(...))` returns the function that stops the diagnostics and puts `bus.on` and `app.mount` back.
3034
3919
 
@@ -3036,6 +3921,6 @@ It changes no behavior: it wraps `bus.on`, `app.mount`, and `app.unmount` and ad
3036
3921
 
3037
3922
  ### Keep the import in production
3038
3923
 
3039
- `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({ app }))` can stay without a guard: it touches nothing and costs a few bytes.
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.
3040
3925
 
3041
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.