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/docs/stores.md CHANGED
@@ -17,7 +17,7 @@ If you do not have a store yet, use `createBus` from the [Guide](guide.md).
17
17
  - [React: sharing a value across components](#react-sharing-a-value-across-components)
18
18
  - [Callbacks on every change](#callbacks-on-every-change)
19
19
  - [Stop watching a store](#stop-watching-a-store)
20
- - [Give the store to every handler](#give-the-store-to-every-handler)
20
+ - [Change the store from any handler](#change-the-store-from-any-handler)
21
21
 
22
22
  ## Wire Redux Toolkit to a bus
23
23
 
@@ -28,23 +28,40 @@ Redux’s `dispatch` → reducers updates **state**. It does not give you first-
28
28
  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).
29
29
 
30
30
  ```js
31
- import { configureStore, createAction } from "@reduxjs/toolkit";
31
+ // bus.js
32
32
  import { createBus } from "nanoeffects";
33
- import { latest } from "nanoeffects/policies";
34
- import { createEffectsMiddleware } from "nanoeffects/redux";
35
33
 
36
- const userRequested = createAction("user/requested"); // RTK
37
- const userLoaded = createAction("user/loaded");
34
+ export const bus = createBus(); // the bus of your app; it can exist before the store
35
+ ```
36
+
37
+ ```js
38
+ // user.js
39
+ import { createAction } from "@reduxjs/toolkit";
38
40
 
39
- const bus = createBus(); // the bus of your app; it can exist before the store
41
+ export const userRequested = createAction("user/requested"); // RTK
42
+ export const userLoaded = createAction("user/loaded");
43
+ ```
44
+
45
+ ```js
46
+ // store.js
47
+ import { configureStore } from "@reduxjs/toolkit";
48
+ import { createEffectsMiddleware } from "nanoeffects/redux";
49
+ import { bus } from "./bus";
40
50
 
41
51
  // createEffectsMiddleware — actions reach the bus after reducers; tasks get ctx.store
42
52
  const effects = createEffectsMiddleware(bus);
43
53
 
44
- const store = configureStore({
54
+ export const store = configureStore({
45
55
  reducer,
46
56
  middleware: (getDefault) => getDefault().concat(effects),
47
57
  });
58
+ ```
59
+
60
+ ```js
61
+ // user-handlers.js
62
+ import { latest } from "nanoeffects/policies";
63
+ import { bus } from "./bus";
64
+ import { userLoaded, userRequested } from "./user";
48
65
 
49
66
  // latest — a newer run aborts the older one
50
67
  bus.on(
@@ -95,28 +112,44 @@ const effects = createEffectsMiddleware(bus);
95
112
  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:
96
113
 
97
114
  - **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.
98
- - **The event changes the store.** The handler reports its result as an event. A listener of the bus writes it into the store.
115
+ - **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.
99
116
 
100
117
  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`.
101
118
 
102
119
  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.
103
120
 
104
- The bus is the same for every store below. Create it once; each subsection imports it.
121
+ 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:
122
+
123
+ - `setQuery(query)` and `setResults(items)` are the mutations. The user's input calls the first, the handler calls the second.
124
+ - `onQueryChanged(callback)` tells you about a change of `query`, and returns the function that stops it.
125
+
126
+ 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.
105
127
 
106
128
  ```js
107
- // search-bus.js
108
- import { createBus } from "nanoeffects";
109
- import { createAction, emitterPlugin } from "nanoeffects/actions";
110
- import { debounce, latest } from "nanoeffects/policies";
129
+ // bus.js
130
+ import { contextPlugin, createBus } from "nanoeffects";
131
+ import { emitterPlugin } from "nanoeffects/actions";
132
+ import { store } from "./store";
133
+
134
+ export const bus = createBus()
135
+ .use(emitterPlugin()) // adds bus.emitter
136
+ .use(contextPlugin({ store })); // ctx.store on every task
137
+ ```
111
138
 
112
- export const bus = createBus().use(emitterPlugin()); // adds bus.emitter; returns the same bus
139
+ ```js
140
+ // search.js
141
+ import { createAction } from "nanoeffects/actions";
113
142
 
114
143
  // createAction — plain action creators; calling one only builds { type, payload }
115
144
  export const queryChanged = createAction("search/queryChanged");
116
145
  export const searchLoaded = createAction("search/loaded");
146
+ ```
117
147
 
118
- // bus.emitter: a callback (query) => bus.emit(queryChanged(query))
119
- export const emitQueryChanged = bus.emitter(queryChanged);
148
+ ```js
149
+ // search-handlers.js
150
+ import { debounce, latest } from "nanoeffects/policies";
151
+ import { bus } from "./bus";
152
+ import { queryChanged, searchLoaded } from "./search";
120
153
 
121
154
  // debounce — wait 300 ms of quiet; latest — abort the search of the previous query
122
155
  bus.on(
@@ -130,141 +163,199 @@ bus.on(
130
163
  }),
131
164
  ),
132
165
  );
166
+
167
+ // event → store: the store comes from ctx, and a mutation changes it
168
+ bus.on(searchLoaded, (items, { store }) => {
169
+ store.setResults(items);
170
+ });
171
+ ```
172
+
173
+ ```js
174
+ // connect.js
175
+ import { bus } from "./bus";
176
+ import { queryChanged } from "./search";
177
+ import { store } from "./store";
178
+
179
+ // store → event: bus.emitter turns the callback into an emit of queryChanged
180
+ export const stopQuery = store.onQueryChanged(bus.emitter(queryChanged));
133
181
  ```
134
182
 
135
183
  Write `results` and never the field you watch: a handler that changed `query` would start itself again. Find your store below.
136
184
 
137
185
  ### Nanostores
138
186
 
139
- `listen` calls back with the new value. `subscribe` would also call at once, with the current value, so use `listen`.
187
+ `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`.
140
188
 
141
189
  ```js
142
- import { atom } from "nanostores";
143
- import { bus, emitQueryChanged, searchLoaded } from "./search-bus";
190
+ // store.js
191
+ import { action, atom } from "nanostores";
144
192
 
145
193
  const $query = atom("");
146
194
  const $results = atom([]);
147
195
 
148
- // store → bus: listen calls back with the new query on every change
149
- const stopQuery = $query.listen(emitQueryChanged);
150
-
151
- // bus → store: bus.on returns the function that removes this listener
152
- const stopResults = bus.on(searchLoaded, (items) => {
153
- $results.set(items);
154
- });
196
+ export const store = {
197
+ // action — a named mutation of an atom
198
+ setQuery: action($query, "setQuery", (target, query) => target.set(query)),
199
+ setResults: action($results, "setResults", (target, items) => target.set(items)),
200
+ // listen — calls back with the new query on every change
201
+ onQueryChanged: (callback) => $query.listen(callback),
202
+ };
155
203
  ```
156
204
 
157
205
  ### MobX
158
206
 
159
- `reaction` watches an expression and calls back with its value. Change observables inside `runInAction`.
207
+ `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.
208
+
209
+ <!-- docs-check: fork -->
160
210
 
161
211
  ```js
162
- import { observable, reaction, runInAction } from "mobx";
163
- import { bus, emitQueryChanged, searchLoaded } from "./search-bus";
212
+ // store.js
213
+ import { makeAutoObservable, reaction } from "mobx";
164
214
 
165
- const search = observable({ query: "", results: [] });
215
+ class SearchStore {
216
+ query = "";
217
+ results = [];
166
218
 
167
- // store → bus: reaction watches the expression and calls back with its value
168
- const stopQuery = reaction(() => search.query, emitQueryChanged);
219
+ constructor() {
220
+ makeAutoObservable(this); // the methods below become actions
221
+ }
169
222
 
170
- // bus → store: bus.on returns the function that removes this listener
171
- const stopResults = bus.on(searchLoaded, (items) => {
172
- runInAction(() => {
173
- search.results = items;
174
- });
175
- });
223
+ setQuery(query) {
224
+ this.query = query;
225
+ }
226
+
227
+ setResults(items) {
228
+ this.results = items;
229
+ }
230
+
231
+ // reaction — watches the expression and calls back with its value
232
+ onQueryChanged(callback) {
233
+ return reaction(() => this.query, callback);
234
+ }
235
+ }
236
+
237
+ export const store = new SearchStore();
176
238
  ```
177
239
 
178
240
  ### Valtio
179
241
 
180
- `subscribeKey` watches one field of a proxy state. Assign to the proxy to change it.
242
+ `subscribeKey` watches one field of a proxy state. A mutation assigns to the proxy.
243
+
244
+ <!-- docs-check: fork -->
181
245
 
182
246
  ```js
247
+ // store.js
183
248
  import { proxy } from "valtio";
184
249
  import { subscribeKey } from "valtio/utils";
185
- import { bus, emitQueryChanged, searchLoaded } from "./search-bus";
186
250
 
187
251
  const state = proxy({ query: "", results: [] });
188
252
 
189
- // store → bus: subscribeKey watches one field
190
- const stopQuery = subscribeKey(state, "query", emitQueryChanged);
191
-
192
- // bus → store: bus.on returns the function that removes this listener
193
- const stopResults = bus.on(searchLoaded, (items) => {
194
- state.results = items;
195
- });
253
+ export const store = {
254
+ setQuery(query) {
255
+ state.query = query;
256
+ },
257
+ setResults(items) {
258
+ state.results = items;
259
+ },
260
+ // subscribeKey — watches one field
261
+ onQueryChanged: (callback) => subscribeKey(state, "query", callback),
262
+ };
196
263
  ```
197
264
 
198
265
  ### Jotai
199
266
 
200
- `sub` gives no value, so read it yourself. Write with `store.set`.
267
+ `sub` gives no value, so read it yourself. A write-only atom is the mutation, and `set` runs it.
268
+
269
+ <!-- docs-check: fork -->
201
270
 
202
271
  ```js
272
+ // store.js
203
273
  import { atom, createStore } from "jotai";
204
- import { bus, emitQueryChanged, searchLoaded } from "./search-bus";
205
274
 
206
275
  const queryAtom = atom("");
207
276
  const resultsAtom = atom([]);
208
- const store = createStore();
209
-
210
- // store → bus: sub calls back without a value; store.get reads it
211
- const stopQuery = store.sub(queryAtom, () => emitQueryChanged(store.get(queryAtom)));
212
-
213
- // bus → store: bus.on returns the function that removes this listener
214
- const stopResults = bus.on(searchLoaded, (items) => {
215
- store.set(resultsAtom, items);
216
- });
277
+ const jotai = createStore();
278
+
279
+ // write-only atoms: the mutations
280
+ const setQueryAtom = atom(null, (_get, set, query) => set(queryAtom, query));
281
+ const setResultsAtom = atom(null, (_get, set, items) => set(resultsAtom, items));
282
+
283
+ export const store = {
284
+ setQuery: (query) => jotai.set(setQueryAtom, query),
285
+ setResults: (items) => jotai.set(setResultsAtom, items),
286
+ // sub calls back without a value; get reads it
287
+ onQueryChanged: (callback) =>
288
+ jotai.sub(queryAtom, () => callback(jotai.get(queryAtom))),
289
+ };
217
290
  ```
218
291
 
219
292
  ### Zustand
220
293
 
221
- 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.
294
+ 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.
295
+
296
+ <!-- docs-check: fork -->
222
297
 
223
298
  ```js
299
+ // store.js
224
300
  import { create } from "zustand";
225
301
  import { subscribeWithSelector } from "zustand/middleware";
226
- import { bus, emitQueryChanged, searchLoaded } from "./search-bus";
227
302
 
228
- const useSearchStore = create(
229
- subscribeWithSelector(() => ({ query: "", results: [] })),
303
+ const useSearch = create(
304
+ subscribeWithSelector((set) => ({
305
+ query: "",
306
+ results: [],
307
+ // the mutations are part of the state
308
+ setQuery: (query) => set({ query }),
309
+ setResults: (items) => set({ results: items }),
310
+ })),
230
311
  );
231
312
 
232
- // store → bus: subscribe with a selector calls back with state.query only when it changes
233
- const stopQuery = useSearchStore.subscribe((state) => state.query, emitQueryChanged);
234
-
235
- // bus → store: bus.on returns the function that removes this listener
236
- const stopResults = bus.on(searchLoaded, (items) => {
237
- useSearchStore.setState({ results: items });
238
- });
313
+ export const store = {
314
+ setQuery: (query) => useSearch.getState().setQuery(query),
315
+ setResults: (items) => useSearch.getState().setResults(items),
316
+ // a selector: called back with state.query only when it changes
317
+ onQueryChanged: (callback) => useSearch.subscribe((state) => state.query, callback),
318
+ };
239
319
  ```
240
320
 
241
321
  ### Pinia
242
322
 
243
- 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.
323
+ 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.
324
+
325
+ <!-- docs-check: fork -->
244
326
 
245
327
  ```js
246
- import { watch } from "vue";
328
+ // store.js
247
329
  import { defineStore } from "pinia";
248
- import { bus, emitQueryChanged, searchLoaded } from "./search-bus";
330
+ import { watch } from "vue";
249
331
 
250
- // defineStore — Pinia: the state of one store
332
+ // defineStore — Pinia: the state and the actions of one store
251
333
  const useSearch = defineStore("search", {
252
334
  state: () => ({ query: "", results: [] }),
335
+ actions: {
336
+ setQuery(query) {
337
+ this.query = query;
338
+ },
339
+ setResults(items) {
340
+ this.results = items;
341
+ },
342
+ },
253
343
  });
254
344
 
255
345
  const search = useSearch(); // call it after the app has installed Pinia
256
346
 
257
- // store → bus: watch calls back with the new query when search.query changes
258
- const stopQuery = watch(() => search.query, emitQueryChanged);
259
-
260
- // bus → store: bus.on returns the function that removes this listener
261
- const stopResults = bus.on(searchLoaded, (items) => {
262
- search.results = items;
263
- });
347
+ export const store = {
348
+ setQuery: (query) => search.setQuery(query),
349
+ setResults: (items) => search.setResults(items),
350
+ // watch — calls back with the new query when search.query changes
351
+ onQueryChanged: (callback) => watch(() => search.query, callback),
352
+ };
264
353
  ```
265
354
 
266
355
  ### React: sharing a value across components
267
356
 
357
+ <!-- docs-check: new -->
358
+
268
359
  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.
269
360
 
270
361
  | The value lives in | Use when |
@@ -277,20 +368,30 @@ A value of your UI (a query, a filter, a name) often has two jobs. Several compo
277
368
 
278
369
  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.
279
370
 
280
- First the bus. `reactPlugin()` adds `bus.useEmitter`, and destructuring it gives a plain hook that you export:
371
+ 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:
281
372
 
282
373
  ```js
283
- // react-search-bus.js
374
+ // bus.js
284
375
  import { createBus } from "nanoeffects";
285
- import { createAction } from "nanoeffects/actions";
286
- import { debounce, latest } from "nanoeffects/policies";
287
376
  import { reactPlugin } from "nanoeffects/react";
288
377
 
289
378
  export const bus = createBus().use(reactPlugin()); // the bus; reactPlugin adds bus.useEmitter
290
379
  export const { useEmitter } = bus; // a free hook: it does not read `this`
380
+ ```
381
+
382
+ ```js
383
+ // search.js
384
+ import { createAction } from "nanoeffects/actions";
291
385
 
292
386
  export const queryChanged = createAction("query/changed");
293
387
  export const searchLoaded = createAction("search/loaded");
388
+ ```
389
+
390
+ ```js
391
+ // search-handlers.js
392
+ import { debounce, latest } from "nanoeffects/policies";
393
+ import { bus } from "./bus";
394
+ import { queryChanged, searchLoaded } from "./search";
294
395
 
295
396
  // debounce — wait 300 ms of quiet; latest — a newer query aborts the older request
296
397
  bus.on(
@@ -310,7 +411,8 @@ Then the provider. It holds the state, calls `useEmitter` once, and listens for
310
411
  ```jsx
311
412
  // SearchProvider.jsx
312
413
  import { createContext, useContext, useEffect, useState } from "react";
313
- import { bus, queryChanged, searchLoaded, useEmitter } from "./react-search-bus";
414
+ import { bus, useEmitter } from "./bus";
415
+ import { queryChanged, searchLoaded } from "./search";
314
416
 
315
417
  const SearchContext = createContext(null);
316
418
 
@@ -367,6 +469,8 @@ Two limits to know:
367
469
 
368
470
  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`:
369
471
 
472
+ <!-- docs-check: new -->
473
+
370
474
  ```jsx
371
475
  // value-store.js
372
476
  import { useSyncExternalStore } from "react";
@@ -396,19 +500,36 @@ export function createValue(initial) {
396
500
  export const useValue = (store) => useSyncExternalStore(store.subscribe, store.get);
397
501
  ```
398
502
 
399
- ```jsx
400
- // search-value.jsx
401
- import { createBus, contextPlugin } from "nanoeffects";
402
- import { createAction, emitterPlugin } from "nanoeffects/actions";
403
- import { createValue, useValue } from "./value-store";
503
+ ```js
504
+ // query.js
505
+ import { createValue } from "./value-store";
404
506
 
405
- const query = createValue("");
507
+ export const query = createValue("");
508
+ ```
509
+
510
+ ```js
511
+ // bus.js
512
+ import { contextPlugin, createBus } from "nanoeffects";
513
+ import { emitterPlugin } from "nanoeffects/actions";
514
+ import { query } from "./query";
406
515
 
407
516
  // emitterPlugin adds bus.emitter; contextPlugin puts `query` on every task as ctx.query
408
- const bus = createBus().use(emitterPlugin()).use(contextPlugin({ query }));
517
+ export const bus = createBus().use(emitterPlugin()).use(contextPlugin({ query }));
518
+ ```
519
+
520
+ ```js
521
+ // search.js
522
+ import { createAction } from "nanoeffects/actions";
409
523
 
410
- const queryChanged = createAction("query/changed");
411
- const refreshed = createAction("search/refreshed");
524
+ export const queryChanged = createAction("query/changed");
525
+ export const refreshed = createAction("search/refreshed");
526
+ ```
527
+
528
+ ```js
529
+ // search-handlers.js
530
+ import { bus } from "./bus";
531
+ import { query } from "./query";
532
+ import { queryChanged, refreshed } from "./search";
412
533
 
413
534
  // store → bus: bus.emitter turns the action into a callback; one place, any number of setters
414
535
  const emitQueryChanged = bus.emitter(queryChanged);
@@ -419,6 +540,12 @@ bus.on(refreshed, async (_payload, ctx) => {
419
540
  const items = await api.search(ctx.query.get(), { signal: ctx.signal });
420
541
  console.log(items.length);
421
542
  });
543
+ ```
544
+
545
+ ```jsx
546
+ // SearchBox.jsx
547
+ import { query } from "./query";
548
+ import { useValue } from "./value-store";
422
549
 
423
550
  // any number of components read and write the same value
424
551
  export function SearchBox() {
@@ -431,16 +558,31 @@ export function SearchBox() {
431
558
 
432
559
  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:
433
560
 
561
+ <!-- docs-check: new -->
562
+
563
+ ```js
564
+ // bus.js
565
+ import { createBus } from "nanoeffects";
566
+ import { emitterPlugin } from "nanoeffects/actions";
567
+
568
+ export const bus = createBus().use(emitterPlugin()); // adds bus.emitter
569
+ ```
570
+
571
+ ```js
572
+ // search.js
573
+ import { createAction } from "nanoeffects/actions";
574
+
575
+ export const queryChanged = createAction("query/changed");
576
+ ```
577
+
434
578
  ```jsx
435
- // search-atom.jsx
579
+ // SearchBox.jsx
436
580
  import { atom } from "nanostores";
437
581
  import { useStore } from "@nanostores/react";
438
- import { createBus } from "nanoeffects";
439
- import { createAction, emitterPlugin } from "nanoeffects/actions";
582
+ import { bus } from "./bus";
583
+ import { queryChanged } from "./search";
440
584
 
441
- const bus = createBus().use(emitterPlugin()); // adds bus.emitter
442
585
  const $query = atom("");
443
- const queryChanged = createAction("query/changed");
444
586
 
445
587
  // listen, not subscribe: listen skips the call with the current value at start
446
588
  $query.listen(bus.emitter(queryChanged));
@@ -455,16 +597,28 @@ export function SearchBox() {
455
597
 
456
598
  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.
457
599
 
458
- ```jsx
459
- // search-reducer.jsx
460
- import { useReducer } from "react";
600
+ <!-- docs-check: new -->
601
+
602
+ ```js
603
+ // bus.js
461
604
  import { createBus } from "nanoeffects";
605
+
606
+ export const bus = createBus(); // the bus: created once
607
+ ```
608
+
609
+ ```js
610
+ // search.js
462
611
  import { createAction } from "nanoeffects/actions";
463
612
 
464
- const bus = createBus(); // the bus: created once
613
+ export const queryChanged = createAction("query/changed");
614
+ export const queryCleared = createAction("query/cleared");
615
+ ```
465
616
 
466
- const queryChanged = createAction("query/changed");
467
- const queryCleared = createAction("query/cleared");
617
+ ```jsx
618
+ // Search.jsx
619
+ import { useReducer } from "react";
620
+ import { bus } from "./bus";
621
+ import { queryChanged, queryCleared } from "./search";
468
622
 
469
623
  function reducer(state, action) {
470
624
  if (queryChanged.match(action)) {
@@ -504,8 +658,8 @@ The stores above call back only when the value changed, so the emitter does not
504
658
 
505
659
  ### Stop watching a store
506
660
 
507
- 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.
661
+ `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.
508
662
 
509
- ### Give the store to every handler
663
+ ### Change the store from any handler
510
664
 
511
- 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.
665
+ 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.