nanoeffects 0.29.0 → 0.31.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 (127) hide show
  1. package/README.md +6 -5
  2. package/dist/{action-creator-jeEqIKgK.d.ts → action-creator-6rlBTmH9.d.ts} +27 -2
  3. package/dist/actions.d.ts +3 -3
  4. package/dist/actions.js +1 -1
  5. package/dist/{bus-0uYn_kRj.js → bus-DFlmNJ6h.js} +1 -1
  6. package/dist/context-B5Z2RpJt.js +1 -0
  7. package/dist/{context-types-Bme1mzkn.d.ts → context-types-DKNc5mkS.d.ts} +1 -1
  8. package/dist/context.d.ts +2 -2
  9. package/dist/dev.d.ts +49 -3
  10. package/dist/dev.js +2 -1
  11. package/dist/effects.d.ts +2 -2
  12. package/dist/effects.js +1 -1
  13. package/dist/emit-key-C6FL7Ewn.js +1 -0
  14. package/dist/guard.d.ts +59 -0
  15. package/dist/guard.js +1 -0
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/match.d.ts +1 -1
  19. package/dist/{module-BWMW83qk.d.ts → module-G39MaVOt.d.ts} +2 -2
  20. package/dist/notrace/{action-creator-xpQ_I2h8.d.ts → action-creator-BrMSO3RP.d.ts} +1 -0
  21. package/dist/notrace/actions.d.ts +2 -2
  22. package/dist/notrace/effects.d.ts +2 -2
  23. package/dist/notrace/index.d.ts +1 -1
  24. package/dist/notrace/{plugin-6LhNx0th.d.ts → plugin-7GUL2EW5.d.ts} +1 -1
  25. package/dist/notrace/policies.d.ts +1 -1
  26. package/dist/notrace/redux.d.ts +1 -1
  27. package/dist/observable.d.ts +1 -1
  28. package/dist/oop.d.ts +1 -1
  29. package/dist/oop.js +1 -1
  30. package/dist/{plugin-BaErevLy.d.ts → plugin-BWNP23hr.d.ts} +2 -2
  31. package/dist/policies.d.ts +2 -2
  32. package/dist/policies.js +1 -1
  33. package/dist/production/dev.js +1 -1
  34. package/dist/react.d.ts +2 -2
  35. package/dist/react.js +1 -1
  36. package/dist/redux.d.ts +1 -1
  37. package/dist/scope.d.ts +2 -2
  38. package/dist/signal.d.ts +22 -2
  39. package/dist/signal.js +1 -1
  40. package/dist/site-DLvUoXs3.js +1 -0
  41. package/dist/svelte.d.ts +1 -1
  42. package/dist/trace.d.ts +151 -2
  43. package/dist/trace.js +3 -1
  44. package/dist/{types-D5xsR37j.d.ts → types-N_cdRr1r.d.ts} +2 -2
  45. package/dist/vue.d.ts +1 -1
  46. package/dist/{with-policy-C6BIiPIi.js → with-policy-CeM1rtES.js} +1 -1
  47. package/dist/{with-policy-BZVRBUOm.d.ts → with-policy-CthiBp_z.d.ts} +1 -1
  48. package/docs/README.md +62 -31
  49. package/docs/actions.md +44 -27
  50. package/docs/cancel-stale-requests.md +2 -0
  51. package/docs/chat-example.md +3 -1
  52. package/docs/chat-widget.md +2 -0
  53. package/docs/context.md +322 -9
  54. package/docs/debounce-and-abort.md +3 -1
  55. package/docs/debugging-loops.md +318 -0
  56. package/docs/effects.md +255 -138
  57. package/docs/errors.md +4 -2
  58. package/docs/examples.md +60 -0
  59. package/docs/guide.md +39 -22
  60. package/docs/hmr.md +35 -1
  61. package/docs/loop-guard.md +109 -0
  62. package/docs/migrating-from-effector.md +4 -2
  63. package/docs/migrating-from-mitt.md +6 -4
  64. package/docs/migrating-from-rxjs.md +2 -0
  65. package/docs/migrating-from-saga.md +2 -0
  66. package/docs/motivation.md +14 -21
  67. package/docs/observable.md +2 -0
  68. package/docs/oop.md +3 -1
  69. package/docs/package.md +4 -1
  70. package/docs/pass-the-signal.md +346 -0
  71. package/docs/plugins.md +22 -5
  72. package/docs/policies.md +73 -46
  73. package/docs/prevent-double-submit.md +3 -1
  74. package/docs/query-libraries.md +3 -1
  75. package/docs/react.md +42 -13
  76. package/docs/recipes.md +528 -0
  77. package/docs/ru/README.md +62 -0
  78. package/docs/ru/actions.md +412 -0
  79. package/docs/ru/cancel-stale-requests.md +106 -0
  80. package/docs/ru/chat-example.md +259 -0
  81. package/docs/ru/chat-widget.md +131 -0
  82. package/docs/ru/context.md +402 -0
  83. package/docs/ru/debounce-and-abort.md +105 -0
  84. package/docs/ru/debugging-loops.md +318 -0
  85. package/docs/ru/effects.md +538 -0
  86. package/docs/ru/errors.md +36 -0
  87. package/docs/ru/examples.md +60 -0
  88. package/docs/ru/guide.md +530 -0
  89. package/docs/ru/hmr.md +83 -0
  90. package/docs/ru/loop-guard.md +109 -0
  91. package/docs/ru/migrating-from-effector.md +339 -0
  92. package/docs/ru/migrating-from-mitt.md +99 -0
  93. package/docs/ru/migrating-from-rxjs.md +197 -0
  94. package/docs/ru/migrating-from-saga.md +97 -0
  95. package/docs/ru/motivation.md +47 -0
  96. package/docs/ru/observable.md +233 -0
  97. package/docs/ru/oop.md +431 -0
  98. package/docs/ru/package.md +42 -0
  99. package/docs/ru/pass-the-signal.md +346 -0
  100. package/docs/ru/plugins.md +398 -0
  101. package/docs/ru/policies.md +201 -0
  102. package/docs/ru/prevent-double-submit.md +122 -0
  103. package/docs/ru/query-libraries.md +392 -0
  104. package/docs/ru/react.md +309 -0
  105. package/docs/ru/recipes.md +528 -0
  106. package/docs/ru/semantics.md +19 -0
  107. package/docs/ru/server.md +204 -0
  108. package/docs/ru/starter.md +118 -0
  109. package/docs/ru/stores.md +673 -0
  110. package/docs/ru/svelte.md +78 -0
  111. package/docs/ru/tracing.md +466 -0
  112. package/docs/ru/typescript.md +170 -0
  113. package/docs/ru/vue.md +92 -0
  114. package/docs/semantics.md +2 -0
  115. package/docs/server.md +2 -0
  116. package/docs/starter.md +2 -0
  117. package/docs/stores.md +2 -0
  118. package/docs/svelte.md +3 -1
  119. package/docs/tracing.md +58 -1
  120. package/docs/typescript.md +2 -0
  121. package/docs/vue.md +3 -1
  122. package/llms-full.txt +2087 -418
  123. package/llms.txt +11 -8
  124. package/package.json +23 -6
  125. package/dist/context-DtUnzC-1.js +0 -1
  126. package/docs/scope.md +0 -278
  127. package/docs/stability.md +0 -90
package/docs/react.md CHANGED
@@ -1,30 +1,34 @@
1
1
  # Build a sign-up form in React
2
2
 
3
- Use this page when a React screen starts requests on user input and you need them not to race: a username check while typing, a submit button, a flaky network. It builds one registration form. The bus lives outside React; the component only subscribes to it.
3
+ > **Level B.** Read first: [Guide](guide.md#1-create-a-bus) steps 1 to 3.
4
+
5
+ The task is a registration form. While the user types a name, the form asks the backend whether it is free, and the "Sign up" button submits the form. That makes the requests race: the answer to an old input can arrive after the answer to a new one and show a wrong result. This page shows how to solve it with nanoeffects. The bus lives outside React; the component only subscribes to it.
6
+
7
+ Steps 1 and 2 are required, step 3 is not. The last two sections are extra.
4
8
 
5
9
  You need a React project and `nanoeffects` installed. The form talks to your own client, `api` below: replace it with your requests.
6
10
 
7
11
  ## On this page
8
12
 
9
- - [See what goes wrong without a bus](#see-what-goes-wrong-without-a-bus)
10
- - [Wire the bus once](#wire-the-bus-once)
11
- - [Connect the form](#connect-the-form)
12
- - [Stop running work when the form closes](#stop-running-work-when-the-form-closes)
13
+ - [Problems of the form without a bus](#problems-of-the-form-without-a-bus)
14
+ - [1. The bus and the handlers](#1-the-bus-and-the-handlers)
15
+ - [2. The form component](#2-the-form-component)
16
+ - [3. Stop requests when the form closes](#3-stop-requests-when-the-form-closes)
13
17
  - [Show the last value of an event as state](#show-the-last-value-of-an-event-as-state)
14
18
  - [Keep a debounced handler between renders](#keep-a-debounced-handler-between-renders)
15
19
  - [Where to go next](#where-to-go-next)
16
20
 
17
- ## See what goes wrong without a bus
21
+ ## Problems of the form without a bus
18
22
 
19
23
  A registration form has three problems that `useEffect` and `fetch` leave to you:
20
24
 
21
- - **A stale answer.** The user types `ann`, then `anna`. The answer for `ann` arrives last and says the name is free, although `anna` is taken.
25
+ - **A race in the name check.** The user types `ann`, then `anna`, and every input sends a "is the name free" request. The answer for `ann` arrives last and says the name is free, although `anna` is taken.
22
26
  - **A double submit.** A second click on "Sign up" sends a second request while the first one is running.
23
27
  - **A flaky network.** One failed request should be retried, but a "name is taken" answer should not.
24
28
 
25
29
  Each has a policy or an effect: `latest`, `leading`, `retry`. They live in one place, outside the component.
26
30
 
27
- ## Wire the bus once
31
+ ## 1. The bus and the handlers
28
32
 
29
33
  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.
30
34
 
@@ -81,7 +85,7 @@ bus.on(
81
85
 
82
86
  The handlers know nothing about React. A test can emit `username/typed` and read `username/checked` without rendering anything: the same wiring is tested in [`examples/registration-form.ts`](../examples/registration-form.ts) and `src/tests/registration-form.test.ts`.
83
87
 
84
- ## Connect the form
88
+ ## 2. The form component
85
89
 
86
90
  <!-- docs-check: continue -->
87
91
 
@@ -152,11 +156,11 @@ The `disabled` button is only for the look. The form stays correct without it: `
152
156
 
153
157
  In development, React StrictMode runs the effect twice (subscribe, unsubscribe, subscribe). That is safe here, because the cleanup removes exactly what the effect added.
154
158
 
155
- ## Stop running work when the form closes
159
+ ## 3. Stop requests when the form closes
156
160
 
157
161
  <!-- docs-check: continue -->
158
162
 
159
- 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`:
163
+ This step is optional. 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`:
160
164
 
161
165
  ```js
162
166
  import { latest } from "nanoeffects/policies";
@@ -278,12 +282,37 @@ export const Search = ({ wait }) => {
278
282
  - Write the policy without its callback: `debounce(300)`, not `debounce(300, callback)`. `usePolicy` puts the callback in itself, always the one of the latest render.
279
283
  - To combine policies, write a function of the task: `(task) => throttle(100, latest(task))`. Here `task` is the callback that `usePolicy` keeps current; pass it, not your own function.
280
284
  - `deps` stay your job: `debounce(wait)` is made again on every render, but only the first one (and the one after `deps` change) is used.
281
- - Without a React component, use [`withPolicy`](policies.md#use-a-policy-without-a-bus).
285
+ - Without a React component, use [`withPolicy`](policies.md#withpolicy-call-a-policy-outside-a-bus).
286
+
287
+ ### Write a policy without its function
288
+
289
+ **Problem.** `usePolicy` puts the callback of the latest render into the policy itself, so it needs a policy that has no function yet. If you passed your own function there, it would be easy to pass the wrong one.
290
+
291
+ Leave the function out and the policy returns a wrapper. The wrapper takes a function and gives back the same policy as the direct call:
292
+
293
+ ```js
294
+ import { debounce } from "nanoeffects/policies";
295
+
296
+ const runSearch = async (query) => {
297
+ await api.search(query);
298
+ };
299
+
300
+ // debounce(300) — no function yet: returns a wrapper
301
+ const wrap = debounce(300);
302
+
303
+ // the wrapper takes the function; same as debounce(300, runSearch)
304
+ const search = wrap(runSearch);
305
+ ```
306
+
307
+ - It works for `latest()`, `leading()`, `debounce(ms)`, `throttle(ms)` and `queue()`, with the options as the last argument: `queue({ concurrency: 2 })`, `debounce(300, { leading: true })`.
308
+ - Each time you apply a wrapper to a function, you get a new policy with its own state.
309
+ - To combine policies, nest them: `throttle(100, latest(fn))`.
310
+ - With `key`, annotate the payload of the key function in TypeScript: `latest({ key: (user: User) => user.id })`. Without the annotation the wrapper cannot know the payload type.
282
311
 
283
312
  ## Where to go next
284
313
 
285
314
  - The policies behind this form: [Policies](policies.md).
286
- - A policy as a plain function without a bus: [Use a policy without a bus](policies.md#use-a-policy-without-a-bus).
315
+ - A policy as a plain function without a bus: [`withPolicy`: call a policy outside a bus](policies.md#withpolicy-call-a-policy-outside-a-bus).
287
316
  - `retry`, `timeout` and the other effects: [Effects](effects.md).
288
317
  - A state library instead of `useState`: [Stores](stores.md).
289
318
  - React Query or SWR instead of your own state: [Use nanoeffects with React Query, SWR and RTK Query](query-libraries.md).
@@ -0,0 +1,528 @@
1
+ # Common tasks
2
+
3
+ > **Levels A to E.** Read first: [README](../README.md).
4
+
5
+ Each task below starts with the problem, then a short solution. Where the page has more to say, the last line links to it. The tasks go from the simplest to the most involved.
6
+
7
+ ## On this page
8
+
9
+ - [Events and handlers](#events-and-handlers)
10
+ - [A handler needs an API client, a store or a logger](#a-handler-needs-an-api-client-a-store-or-a-logger)
11
+ - [One handler fails, the others must still run](#one-handler-fails-the-others-must-still-run)
12
+ - [A typo in an event name](#a-typo-in-an-event-name)
13
+ - [Races and repeated actions in the UI](#races-and-repeated-actions-in-the-ui)
14
+ - [An old response overwrites a new one](#an-old-response-overwrites-a-new-one)
15
+ - [A search box sends a request on every key](#a-search-box-sends-a-request-on-every-key)
16
+ - [A second click sends a second request](#a-second-click-sends-a-second-request)
17
+ - [Messages must go out in order](#messages-must-go-out-in-order)
18
+ - [A "Delete" button in a list blocks the other rows](#a-delete-button-in-a-list-blocks-the-other-rows)
19
+ - [An unreliable network and long flows](#an-unreliable-network-and-long-flows)
20
+ - [A request fails sometimes: retry and backoff](#a-request-fails-sometimes-retry-and-backoff)
21
+ - [A request hangs: timeout](#a-request-hangs-timeout)
22
+ - [Refresh data every N seconds: poll](#refresh-data-every-n-seconds-poll)
23
+ - [Wait for another event inside a handler: take](#wait-for-another-event-inside-a-handler-take)
24
+ - [Load several things at once: all](#load-several-things-at-once-all)
25
+ - [Take the fastest answer: race](#take-the-fastest-answer-race)
26
+ - [Features and domain logic](#features-and-domain-logic)
27
+ - [Get the result of emit](#get-the-result-of-emit)
28
+ - [Group a feature with its handlers: modules](#group-a-feature-with-its-handlers-modules)
29
+ - [Do not leave subscriptions after a widget closes](#do-not-leave-subscriptions-after-a-widget-closes)
30
+ - [Stop work on a server](#stop-work-on-a-server)
31
+ - [Debugging: what ran and why](#debugging-what-ran-and-why)
32
+ - [Other libraries in the project](#other-libraries-in-the-project)
33
+ - [Classes and TypeScript](#classes-and-typescript)
34
+ - [Use the bus in a class](#use-the-bus-in-a-class)
35
+ - [Keep the types of the bus in many files](#keep-the-types-of-the-bus-in-many-files)
36
+ - [Examples of whole apps](#examples-of-whole-apps)
37
+
38
+ ## Events and handlers
39
+
40
+ ### A handler needs an API client, a store or a logger
41
+
42
+ Many handlers use the same `api` client or logger. Importing it in every handler file ties the handlers to one client and makes tests harder. Put it on the bus once, and every task gets it on `ctx`.
43
+
44
+ ```js
45
+ import { createBus } from "nanoeffects";
46
+
47
+ // createBus({ context }) — every task gets these fields on ctx
48
+ const bus = createBus({ context: { api, logger } });
49
+
50
+ bus.on("save", async (draft, { api, logger }) => {
51
+ await api.save(draft);
52
+ logger.info("saved");
53
+ });
54
+ ```
55
+
56
+ More: [Context fields](context.md).
57
+
58
+ ### One handler fails, the others must still run
59
+
60
+ Two handlers listen to `order/placed`: one sends analytics, the other saves the order. By default the first error stops the loop, so a broken analytics call can keep the order from being saved. With `onError` every handler runs and the errors go to one place.
61
+
62
+ ```js
63
+ import { createBus } from "nanoeffects";
64
+
65
+ const bus = createBus({
66
+ // onError — isolates the handlers: a thrown error goes here and the next handler still runs
67
+ onError: (error, { type }) => report(error, type),
68
+ });
69
+
70
+ const sendAnalytics = (order) => {
71
+ throw new Error(`analytics is down for ${order.id}`);
72
+ };
73
+ const saveOrder = (order) => console.log("saved", order.id);
74
+
75
+ bus.on("order/placed", sendAnalytics); // throws
76
+ bus.on("order/placed", saveOrder); // still runs
77
+
78
+ bus.emit("order/placed", { id: 1 }); // saved 1
79
+ ```
80
+
81
+ More: [Errors and unheard events](errors.md#isolate-failing-handlers).
82
+
83
+ ### A typo in an event name
84
+
85
+ `emit("serch")` does not fail: no one listens to that name, so nothing happens, and you look for the bug in the handler. `onUnhandled` warns about an event that nobody heard. Turn it on in development.
86
+
87
+ ```js
88
+ import { createBus } from "nanoeffects";
89
+
90
+ const bus = createBus({
91
+ // onUnhandled — called when an event has no listeners
92
+ onUnhandled: ({ type }) => console.warn(`Nobody listens to "${type}"`),
93
+ });
94
+
95
+ bus.on("search", (query) => console.log(query));
96
+
97
+ bus.emit("serch", "nano"); // warning: Nobody listens to "serch"
98
+ ```
99
+
100
+ More: [Warn when nobody listens](errors.md#warn-when-nobody-listens).
101
+
102
+ ## Races and repeated actions in the UI
103
+
104
+ ### An old response overwrites a new one
105
+
106
+ A user types `re`, then `react`. Two requests are in flight. The server answers `react` fast and `re` slowly, so the answer for `re` arrives last and replaces the right list with the wrong one. This is what a plain React effect does:
107
+
108
+ ```jsx
109
+ import { useEffect, useState } from "react";
110
+
111
+ export function Search({ query }) {
112
+ const [items, setItems] = useState([]);
113
+
114
+ useEffect(() => {
115
+ // two requests are in flight, and the one that finishes last wins, even the older one
116
+ api.search(query).then(setItems);
117
+ }, [query]);
118
+
119
+ return (
120
+ <ul>
121
+ {items.map((item) => (
122
+ <li key={item.id}>{item.title}</li>
123
+ ))}
124
+ </ul>
125
+ );
126
+ }
127
+ ```
128
+
129
+ Move the request into a handler with `latest`: a new `search` aborts the previous request, and the aborted one delivers no result.
130
+
131
+ ```js
132
+ import { createBus } from "nanoeffects";
133
+ import { latest } from "nanoeffects/policies";
134
+
135
+ const bus = createBus();
136
+
137
+ // latest — a new "search" aborts the previous one and drops its result
138
+ bus.on(
139
+ "search",
140
+ latest(async (query, { signal }) => {
141
+ // signal — lets the browser cancel the request itself
142
+ const items = await api.search(query, { signal });
143
+ bus.emit("results", items);
144
+ }),
145
+ );
146
+
147
+ bus.on("results", (items) => render(items));
148
+ ```
149
+
150
+ More: [Cancel stale requests](cancel-stale-requests.md), [Policies](policies.md#latest-abort-the-previous-run), and the component side in [Build a sign-up form in React](react.md).
151
+
152
+ ### A search box sends a request on every key
153
+
154
+ Typing `react` sends five requests, and the list flickers. `debounce` waits for a pause in typing; `latest` aborts the request that is still in flight.
155
+
156
+ ```js
157
+ import { createBus } from "nanoeffects";
158
+ import { debounce, latest } from "nanoeffects/policies";
159
+
160
+ const bus = createBus();
161
+
162
+ // latest — a new search aborts the request in flight
163
+ const searchLatest = latest(async (query, { signal }) => {
164
+ const items = await api.search(query, { signal });
165
+ bus.emit("results", items);
166
+ });
167
+
168
+ // debounce — start after 300 ms without a new "typed"
169
+ bus.on("typed", debounce(300, searchLatest));
170
+ ```
171
+
172
+ More: [Pause between requests](debounce-and-abort.md).
173
+
174
+ ### A second click sends a second request
175
+
176
+ A user double-clicks "Sign up" on a slow network, and two accounts are created. `leading` drops every call that comes while the first one still runs.
177
+
178
+ ```js
179
+ import { createBus } from "nanoeffects";
180
+ import { leading } from "nanoeffects/policies";
181
+
182
+ const bus = createBus();
183
+
184
+ // leading — a call that comes while the previous one runs is dropped
185
+ bus.on(
186
+ "form/submitted",
187
+ leading(async (form, { signal }) => {
188
+ await api.signUp(form, { signal });
189
+ }),
190
+ );
191
+ ```
192
+
193
+ More: [Prevent a double submit](prevent-double-submit.md).
194
+
195
+ ### Messages must go out in order
196
+
197
+ In a chat, a user sends `Hello` and then `How are you?` within a second. Two parallel requests can reach the server in the wrong order. `queue` sends the next message only after the previous one is delivered.
198
+
199
+ ```js
200
+ import { createBus } from "nanoeffects";
201
+ import { queue } from "nanoeffects/policies";
202
+
203
+ const bus = createBus();
204
+
205
+ // queue — the next message waits until the previous one is delivered
206
+ bus.on(
207
+ "message/sent",
208
+ queue(async (message, { signal }) => {
209
+ await api.sendMessage(message, { signal });
210
+ }),
211
+ );
212
+ ```
213
+
214
+ More: [Policies, `queue`](policies.md#run-calls-one-after-another-with-queue).
215
+
216
+ ### A "Delete" button in a list blocks the other rows
217
+
218
+ You protect "Delete" with `leading`. While one row is deleted, clicks on the other rows are dropped too, because the whole handler has one state. `key` gives every row its own state.
219
+
220
+ ```js
221
+ import { createBus } from "nanoeffects";
222
+ import { leading } from "nanoeffects/policies";
223
+
224
+ const bus = createBus();
225
+
226
+ // leading with key — every row id has its own state, so other rows are not blocked
227
+ bus.on(
228
+ "row/deleted",
229
+ leading(
230
+ async (row, { signal }) => {
231
+ await api.deleteRow(row.id, { signal });
232
+ },
233
+ { key: (row) => row.id },
234
+ ),
235
+ );
236
+ ```
237
+
238
+ More: [Policies, `key`](policies.md#key-isolate-state-by-key).
239
+
240
+ ## An unreliable network and long flows
241
+
242
+ ### A request fails sometimes: retry and backoff
243
+
244
+ A request fails once in a while because of the network. `retry` repeats it. If the server is overloaded, repeating at once makes it worse, so use `backoff` to make the pause longer with each attempt.
245
+
246
+ ```js
247
+ import { createBus } from "nanoeffects";
248
+ import { backoff, retry } from "nanoeffects/effects";
249
+
250
+ const bus = createBus();
251
+
252
+ // retry — repeat the request after an error, up to 4 attempts
253
+ const loadWithRetry = retry((id, { signal }) => api.getUser(id, { signal }), {
254
+ times: 4,
255
+ // backoff — the pause grows (200, 400, 800 ms) with a random part, so clients do not retry at the same moment
256
+ delay: backoff({
257
+ base: 200,
258
+ max: 5000,
259
+ jitter: true,
260
+ }),
261
+ });
262
+
263
+ bus.on("user/opened", loadWithRetry);
264
+ ```
265
+
266
+ More: [`backoff`](effects.md#backoff).
267
+
268
+ ### A request hangs: timeout
269
+
270
+ A request does not fail and does not answer, and the spinner spins forever. `timeout` rejects the call after the time you set and aborts the request.
271
+
272
+ ```js
273
+ import { createBus } from "nanoeffects";
274
+ import { timeout } from "nanoeffects/effects";
275
+
276
+ const bus = createBus();
277
+
278
+ // timeout — reject with TimeoutError and abort the request after 5 s
279
+ const loadOnce = timeout(5000, (id, { signal }) => api.getUser(id, { signal }));
280
+
281
+ bus.on("user/opened", loadOnce);
282
+ ```
283
+
284
+ More: [`timeout`](effects.md#timeout).
285
+
286
+ ### Refresh data every N seconds: poll
287
+
288
+ A dashboard must refresh its numbers every ten seconds while it is open. `poll` is an effect: it calls your function on a timer and ends when the task is aborted, so a closed dashboard does not keep asking.
289
+
290
+ ```js
291
+ import { createBus } from "nanoeffects";
292
+ import { poll } from "nanoeffects/effects";
293
+
294
+ const bus = createBus();
295
+
296
+ bus.on("dashboard/opened", async (_, ctx) => {
297
+ // stop the polling when the user leaves
298
+ ctx.on("dashboard/closed", () => ctx.abort());
299
+
300
+ // poll — call refresh every 10 s until the task is aborted
301
+ await poll(ctx, () => refresh(), 10_000);
302
+ });
303
+ ```
304
+
305
+ More: [`poll`](effects.md#poll).
306
+
307
+ ### Wait for another event inside a handler: take
308
+
309
+ After a login the handler must wait for the user to log out before it cleans up. Instead of a flag and a second handler, `take` pauses the handler until the next event with that name.
310
+
311
+ ```js
312
+ import { createBus } from "nanoeffects";
313
+ import { take } from "nanoeffects/effects";
314
+
315
+ const bus = createBus();
316
+
317
+ bus.on("login", async (user, ctx) => {
318
+ // take — pause this handler until the next "logout"
319
+ const reason = await take(ctx, "logout");
320
+ cleanup(user, reason);
321
+ });
322
+ ```
323
+
324
+ More: [`take`](effects.md#take).
325
+
326
+ ### Load several things at once: all
327
+
328
+ A profile page needs the user and the posts. Loading them one after another is slow. `all` runs both requests together and aborts the second one if the first fails.
329
+
330
+ ```js
331
+ import { createBus } from "nanoeffects";
332
+ import { all } from "nanoeffects/effects";
333
+
334
+ const bus = createBus();
335
+
336
+ bus.on("profile/opened", async (id, ctx) => {
337
+ // all — run both requests together; if one fails, the other is aborted. child carries the signal of its own request
338
+ const { user, posts } = await all(ctx, {
339
+ user: (child) => fetchUser(id, child),
340
+ posts: (child) => fetchPosts(id, child),
341
+ });
342
+
343
+ ctx.emit("profile/loaded", { user, posts });
344
+ });
345
+ ```
346
+
347
+ ### Take the fastest answer: race
348
+
349
+ The same data lives on two servers, and you want the answer of the faster one. `race` returns the first answer and aborts the other request.
350
+
351
+ ```js
352
+ import { createBus } from "nanoeffects";
353
+ import { race } from "nanoeffects/effects";
354
+
355
+ const bus = createBus();
356
+
357
+ bus.on("user/opened", async (id, ctx) => {
358
+ // race — the first answer wins, the slower request is aborted
359
+ const result = await race(ctx, {
360
+ primary: (child) => api.getUser(id, child),
361
+ backup: (child) => api.getUserFromBackup(id, child),
362
+ });
363
+
364
+ const user = "primary" in result ? result.primary : result.backup;
365
+ ctx.emit("user/loaded", user);
366
+ });
367
+ ```
368
+
369
+ More: [`all`](effects.md#all), [`race`](effects.md#race).
370
+
371
+ ## Features and domain logic
372
+
373
+ ### Get the result of emit
374
+
375
+ `emit` of a plain event returns nothing, so to get an answer you need a second event. An action with a handler returns the result of its handler right from `emit`.
376
+
377
+ ```js
378
+ import { createBus } from "nanoeffects";
379
+ import { createAction } from "nanoeffects/actions";
380
+
381
+ const bus = createBus();
382
+
383
+ // createAction with a handler — emit runs it and returns what it returns
384
+ const double = createAction("math/double", (value) => value * 2);
385
+
386
+ const result = bus.emit(double(21)); // 42
387
+ ```
388
+
389
+ More: [Actions, jobs and modules](actions.md).
390
+
391
+ ### Group a feature with its handlers: modules
392
+
393
+ A feature has several subscriptions, and you must remember to remove each one. A module registers them together and removes them all with one `unmount`.
394
+
395
+ ```js
396
+ import { createBus } from "nanoeffects";
397
+ import { createModule, modulePlugin } from "nanoeffects/actions";
398
+
399
+ // createModule — one feature; unmount cancels everything it registered
400
+ const greeting = createModule("greeting", (m) => {
401
+ m.on("greet", (name) => console.log(`hello ${name}`));
402
+ });
403
+
404
+ const bus = createBus();
405
+ const app = bus.use(modulePlugin([greeting]));
406
+
407
+ app.unmount(greeting); // the subscription is gone
408
+ ```
409
+
410
+ More: [Modules](actions.md#modules) and [the guide](guide.md#9-mount-a-domain-as-a-module).
411
+
412
+ ### Do not leave subscriptions after a widget closes
413
+
414
+ A chat widget subscribes to the bus when it opens. If it closes without unsubscribing, the handlers stay: memory grows with every open, and they still update a widget that is gone. A module that you mount on open and unmount on close leaves nothing behind.
415
+
416
+ ```js
417
+ import { createBus } from "nanoeffects";
418
+ import { createModule, modulePlugin } from "nanoeffects/actions";
419
+
420
+ // the module owns the listeners of the widget
421
+ const chatModule = createModule("chat", (m) => {
422
+ m.on("message/received", (message) => render(message));
423
+ });
424
+
425
+ const bus = createBus();
426
+ const app = bus.use(modulePlugin());
427
+
428
+ const stop = app.mount(chatModule); // the widget is opened: the listeners are on the bus
429
+
430
+ stop(); // the widget is closed: no listeners are left
431
+ console.log(bus.get("message/received").length); // 0
432
+ ```
433
+
434
+ More: [Build a chat widget](chat-widget.md).
435
+
436
+ ## Stop work on a server
437
+
438
+ A client closes the tab, but the server goes on asking other services for it. Create an `AbortController` for the request, abort it when the connection closes, and pass its signal to `emit`: `timeout`, `retry` and the request itself stop.
439
+
440
+ ```js
441
+ import { createBus } from "nanoeffects";
442
+ import { createAction } from "nanoeffects/actions";
443
+ import { signalPlugin } from "nanoeffects/signal";
444
+
445
+ // signalPlugin — emit accepts { signal }
446
+ const bus = createBus().use(signalPlugin());
447
+
448
+ const getUser = createAction("user/get", (id, { signal }) => api.getUser(id, { signal }));
449
+
450
+ export async function handleRequest(request, reply) {
451
+ const controller = new AbortController();
452
+
453
+ // close — the client left: abort everything started for this request
454
+ reply.raw.on("close", () => controller.abort());
455
+
456
+ // the task, and the requests it made, stop with the controller
457
+ return bus.emit(getUser(request.params.id), { signal: controller.signal });
458
+ }
459
+ ```
460
+
461
+ More: [Stop work on a server](server.md), including WebSocket and queue workers.
462
+
463
+ ## Debugging: what ran and why
464
+
465
+ An effect fired and you cannot tell which event caused it. The tracer logs every task with its parent, so you see the chain of events.
466
+
467
+ ```js
468
+ import { createBus } from "nanoeffects";
469
+ import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
470
+
471
+ // createTracer — send every task to the console
472
+ const trace = createTracer(consoleLogger());
473
+
474
+ // tracePlugin — also report every emit
475
+ const bus = createBus({ trace }).use(tracePlugin());
476
+
477
+ bus.on("search", (query, ctx) => ctx.emit("results", [query]));
478
+ bus.emit("search", "nano"); // the log shows: search · emit, then results · emit with search as the cause
479
+ ```
480
+
481
+ More: [Trace tasks](tracing.md). To reload modules without a page refresh, see [Hot reload](hmr.md).
482
+
483
+ ## Other libraries in the project
484
+
485
+ - **Redux Toolkit, Zustand, MobX or Pinia.** [Connect a store](stores.md).
486
+ - **React Query, SWR or RTK Query.** [Query libraries](query-libraries.md).
487
+ - **RxJS.** [Use nanoeffects next to RxJS](migrating-from-rxjs.md) and [Observables](observable.md).
488
+ - **Moving from redux-saga.** [Coming from redux-saga](migrating-from-saga.md).
489
+ - **Moving from Effector.** [Coming from Effector](migrating-from-effector.md).
490
+ - **Moving from mitt or nanoevents.** [Coming from mitt](migrating-from-mitt.md).
491
+
492
+ ## Classes and TypeScript
493
+
494
+ ### Use the bus in a class
495
+
496
+ Your logic lives in classes, and each instance must remove its subscriptions when it is no longer needed. `Emitter` gives every instance its own bus and a `dispose` that removes all of its subscriptions.
497
+
498
+ ```js
499
+ import { Emitter } from "nanoeffects/oop";
500
+
501
+ // Emitter — every instance gets its own bus with on, once, off, emit and dispose
502
+ class VideoService extends Emitter {
503
+ constructor() {
504
+ super();
505
+ this.on("played", () => console.log("playing"));
506
+ }
507
+ }
508
+
509
+ const video = new VideoService();
510
+ video.emit("played");
511
+
512
+ video.dispose(); // removes every subscription of this instance
513
+ ```
514
+
515
+ More: [Use the bus from a class](oop.md).
516
+
517
+ ### Keep the types of the bus in many files
518
+
519
+ Handlers and modules are in different files, and TypeScript must know the payloads and the fields of `ctx`. See [TypeScript in many files](typescript.md).
520
+
521
+ ## Examples of whole apps
522
+
523
+ - **A form with a username check and a submit, in React.** [Build a sign-up form in React](react.md).
524
+ - **A whole app in layers: entities, features, modules, a server.** [Build a chat](chat-example.md).
525
+ - **A widget that listens only while it is open.** [Build a chat widget](chat-widget.md).
526
+ - **Vue.** [Use nanoeffects in a Vue app](vue.md).
527
+ - **Svelte.** [Use nanoeffects in a Svelte app](svelte.md).
528
+ - **A ready project.** [Start a project from a template](starter.md).