nanoeffects 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +2 -2
  2. package/dist/{action-creator-Bd4TBZgl.d.ts → action-creator-BrZF6hXc.d.ts} +0 -4
  3. package/dist/actions.d.ts +41 -25
  4. package/dist/actions.js +1 -1
  5. package/dist/bus-CkTHUAvq.js +1 -0
  6. package/dist/context-COgyNlrC.js +1 -0
  7. package/dist/{context-plugin-BVNKqkIx.d.ts → context-plugin--HkNN1ha.d.ts} +1 -1
  8. package/dist/dev.d.ts +9 -8
  9. package/dist/dev.js +1 -1
  10. package/dist/effects.d.ts +2 -2
  11. package/dist/effects.js +1 -1
  12. package/dist/index.d.ts +2 -2
  13. package/dist/index.js +1 -1
  14. package/dist/match.d.ts +50 -0
  15. package/dist/match.js +1 -0
  16. package/dist/{module-DZ5ngUrM.d.ts → module-DqKegXHt.d.ts} +43 -18
  17. package/dist/notrace/{action-creator-D-y_-xRS.d.ts → action-creator-DHAEgwuh.d.ts} +0 -4
  18. package/dist/notrace/actions.d.ts +90 -39
  19. package/dist/notrace/actions.js +1 -1
  20. package/dist/notrace/bus-BmCTo8BG.js +1 -0
  21. package/dist/notrace/{context-plugin-DDfsaErA.d.ts → context-plugin-BapQkGwQ.d.ts} +1 -1
  22. package/dist/notrace/context-yYU3_9lq.js +1 -0
  23. package/dist/notrace/{delay-B_W1tnkS.js → delay-BosJKFjg.js} +1 -1
  24. package/dist/notrace/effects.d.ts +2 -2
  25. package/dist/notrace/effects.js +1 -1
  26. package/dist/notrace/index.d.ts +2 -2
  27. package/dist/notrace/index.js +1 -1
  28. package/dist/notrace/{plugin-CDAngHwg.d.ts → plugin-Cxadz6uU.d.ts} +2 -2
  29. package/dist/notrace/policies.d.ts +30 -2
  30. package/dist/notrace/policies.js +1 -1
  31. package/dist/notrace/redux.d.ts +1 -1
  32. package/dist/observable.d.ts +1 -1
  33. package/dist/oop.d.ts +131 -0
  34. package/dist/oop.js +1 -0
  35. package/dist/{plugin-DQNPBcxx.d.ts → plugin-DkXCvsmB.d.ts} +2 -2
  36. package/dist/policies.d.ts +3 -2
  37. package/dist/policies.js +1 -1
  38. package/dist/react.d.ts +33 -1
  39. package/dist/react.js +1 -1
  40. package/dist/redux.d.ts +1 -1
  41. package/dist/scope.d.ts +5 -6
  42. package/dist/scope.js +1 -1
  43. package/dist/signal.d.ts +51 -0
  44. package/dist/signal.js +1 -0
  45. package/dist/trace.d.ts +1 -1
  46. package/dist/{types-BJ0fzV1p.d.ts → types-KavbvHix.d.ts} +13 -3
  47. package/dist/with-policy-CqUS-VWW.d.ts +30 -0
  48. package/dist/with-policy-DBV9dh2F.js +1 -0
  49. package/docs/README.md +7 -0
  50. package/docs/actions.md +152 -39
  51. package/docs/cancel-stale-requests.md +111 -0
  52. package/docs/debounce-and-abort.md +106 -0
  53. package/docs/guide.md +143 -19
  54. package/docs/hmr.md +6 -5
  55. package/docs/migrating-from-effector.md +51 -18
  56. package/docs/migrating-from-mitt.md +97 -0
  57. package/docs/migrating-from-rxjs.md +17 -6
  58. package/docs/motivation.md +41 -0
  59. package/docs/observable.md +124 -10
  60. package/docs/oop.md +286 -0
  61. package/docs/package.md +6 -2
  62. package/docs/plugins.md +247 -51
  63. package/docs/policies.md +33 -0
  64. package/docs/prevent-double-submit.md +128 -0
  65. package/docs/query-libraries.md +124 -0
  66. package/docs/react.md +61 -5
  67. package/docs/scope.md +60 -22
  68. package/docs/stores.md +263 -109
  69. package/llms-full.txt +2153 -355
  70. package/llms.txt +10 -3
  71. package/package.json +43 -4
  72. package/skills/nanoeffects/SKILL.md +52 -0
  73. package/dist/bus-BAs9x7IQ.js +0 -1
  74. package/dist/context-CK0ZomWt.js +0 -1
  75. package/dist/notrace/bus-Bdk0YyFQ.js +0 -1
  76. package/dist/notrace/context-CctVmHLC.js +0 -1
package/docs/react.md CHANGED
@@ -16,19 +16,26 @@ Each has a policy or an effect: `latest`, `leading`, `retry`. They live in one p
16
16
 
17
17
  ## Wire the bus once
18
18
 
19
- 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.
19
+ 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.
20
20
 
21
21
  ```js
22
- // registration.js
22
+ // bus.js
23
23
  import { createBus } from "nanoeffects";
24
+
25
+ export const bus = createBus(); // the bus: created once, at the top of the module
26
+ ```
27
+
28
+ The handlers know the bus and the bus knows nothing of them. They register when `registration.js` is imported, once, when the app starts:
29
+
30
+ ```js
31
+ // registration.js
24
32
  import { retry } from "nanoeffects/effects";
25
33
  import { debounce, latest, leading } from "nanoeffects/policies";
34
+ import { bus } from "./bus";
26
35
 
27
36
  // fetch rejects with a TypeError when the network fails; an HTTP error is a plain Error
28
37
  const isNetworkError = (error) => error instanceof TypeError;
29
38
 
30
- export const bus = createBus(); // the bus: created once, at the top of the module
31
-
32
39
  // debounce — wait 300 ms of quiet; latest — abort the check of the previous name
33
40
  bus.on(
34
41
  "username/typed",
@@ -66,12 +73,15 @@ The handlers know nothing about React. A test can emit `username/typed` and read
66
73
 
67
74
  ## Connect the form
68
75
 
76
+ <!-- docs-check: continue -->
77
+
69
78
  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.
70
79
 
71
80
  ```jsx
72
81
  // SignUpForm.jsx
73
82
  import { useEffect, useState } from "react";
74
- import { bus } from "./registration"; // the bus of the previous snippet
83
+ import { bus } from "./bus"; // the bus of the previous snippet
84
+ import "./registration"; // registers the handlers; the app does this once, when it starts
75
85
 
76
86
  export function SignUpForm() {
77
87
  const [name, setName] = useState("");
@@ -134,6 +144,8 @@ In development, React StrictMode runs the effect twice (subscribe, unsubscribe,
134
144
 
135
145
  ## Stop running work when the form closes
136
146
 
147
+ <!-- docs-check: continue -->
148
+
137
149
  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`:
138
150
 
139
151
  ```js
@@ -150,8 +162,52 @@ const checkName = latest(async (name, ctx) => {
150
162
 
151
163
  In the component, add `bus.emit("form/closed")` to the cleanup function of the effect. This is optional: skip it when finishing a request after unmount does no harm, as for a cheap check.
152
164
 
165
+ ## Keep a debounced handler between renders
166
+
167
+ **Problem.** A component runs again on every render. A policy made in its body, such as `debounce(300, search)`, is made again each time and loses its timer. A policy made once keeps the callback of the first render, which reads old props and state.
168
+
169
+ **When.** A handler in a component needs a policy, and you do not need a bus: a search box, an autosave, a submit that must not fire twice.
170
+
171
+ `usePolicy(callback, make, deps)` keeps one function between renders. It calls the `callback` of the latest render, so an inline callback sees fresh state. It cancels pending calls when the component unmounts.
172
+
173
+ ```jsx
174
+ import { useState } from "react";
175
+ import { debounce } from "nanoeffects/policies";
176
+ import { usePolicy } from "nanoeffects/react";
177
+
178
+ export const Search = ({ wait }) => {
179
+ const [items, setItems] = useState([]);
180
+
181
+ // usePolicy — a stable function; the first argument is the latest render's callback
182
+ const onChange = usePolicy(
183
+ async (query, { signal }) => {
184
+ const found = await api.search(query, { signal });
185
+ setItems(found);
186
+ },
187
+ // the second argument builds the policy once; task runs the callback above
188
+ (task) => debounce(wait, task),
189
+ // deps — list what the second argument reads; a new wait makes a new policy
190
+ [wait],
191
+ );
192
+
193
+ return (
194
+ <div>
195
+ <input onChange={(event) => onChange(event.target.value)} />
196
+ <p>{items.length} found</p>
197
+ </div>
198
+ );
199
+ };
200
+ ```
201
+
202
+ - `onChange` keeps its identity while `wait` stays the same, so it is safe in `useEffect` and in props.
203
+ - A pending call is cancelled on unmount and when `deps` change.
204
+ - Do not write `usePolicy(debounce(300, callback))`. The policy would be made on every render or keep a stale callback.
205
+ - Without a React component, use [`withPolicy`](policies.md#use-a-policy-without-a-bus).
206
+
153
207
  ## Where to go next
154
208
 
155
209
  - The policies behind this form: [Policies](policies.md).
210
+ - A policy as a plain function without a bus: [Use a policy without a bus](policies.md#use-a-policy-without-a-bus).
156
211
  - `retry`, `timeout` and the other effects: [Effects](effects.md).
157
212
  - A state library instead of `useState`: [Stores](stores.md).
213
+ - React Query or SWR instead of your own state: [Use nanoeffects with React Query and SWR](query-libraries.md).
package/docs/scope.md CHANGED
@@ -11,27 +11,29 @@ You give a scope to the thing that uses it. A call can replace it. Nothing else
11
11
  Scopes are a plugin, so a bus that does not use them pays nothing.
12
12
 
13
13
  ```js
14
+ // bus.js
14
15
  import { createBus } from "nanoeffects";
15
16
  import { scopePlugin } from "nanoeffects/scope";
16
17
 
17
18
  // scopePlugin — makes the scopes below work on this bus; returns the same bus
18
- const bus = createBus().use(scopePlugin());
19
+ export const bus = createBus().use(scopePlugin());
19
20
  ```
20
21
 
22
+ 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.
23
+
21
24
  - Without `scopePlugin` a scope is **ignored**: no error and no warning. If a field is missing from `ctx`, check this first.
22
- - 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.
25
+ - Use it in any order with the other plugins: a scope wins over `effectsPlugin`, `contextPlugin` and `createBus({ context })`.
23
26
 
24
27
  ## Give a command its own fields
25
28
 
29
+ <!-- docs-check: continue -->
30
+
26
31
  ```js
27
- import { createBus } from "nanoeffects";
32
+ // user.js
28
33
  import { createCommand } from "nanoeffects/actions";
29
- import { scopePlugin } from "nanoeffects/scope";
30
-
31
- const bus = createBus().use(scopePlugin());
32
34
 
33
35
  // createCommand — third argument is the scope of this command
34
- const getUser = createCommand(
36
+ export const getUser = createCommand(
35
37
  "user/get",
36
38
  (id, { api, cache }) => {
37
39
  // api and cache are on ctx for this command only
@@ -39,6 +41,11 @@ const getUser = createCommand(
39
41
  },
40
42
  { api, cache: new Map() },
41
43
  );
44
+ ```
45
+
46
+ ```js
47
+ import { bus } from "./bus";
48
+ import { getUser } from "./user";
42
49
 
43
50
  const user = await bus.emit(getUser("1")); // the handler has api and cache
44
51
  ```
@@ -50,18 +57,15 @@ The listeners of `"user/get"` get the same fields when the command is emitted, b
50
57
  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.
51
58
 
52
59
  ```js
53
- import { createBus } from "nanoeffects";
60
+ // feed.js
54
61
  import { createCommand } from "nanoeffects/actions";
55
62
  import { delay, race } from "nanoeffects/effects";
56
- import { scopePlugin } from "nanoeffects/scope";
57
-
58
- const bus = createBus().use(scopePlugin());
59
63
 
60
64
  // your own effect: first parameter is the task (only its signal is used)
61
65
  const fetchJson = (ctx, url) =>
62
66
  fetch(url, { signal: ctx.signal }).then((response) => response.json());
63
67
 
64
- const loadFeed = createCommand(
68
+ export const loadFeed = createCommand(
65
69
  "feed/load",
66
70
  (url, { fetchJson, race, delay }) =>
67
71
  // race, delay, fetchJson — bound to this task: no ctx argument
@@ -72,6 +76,19 @@ const loadFeed = createCommand(
72
76
  // $effects — built-in and your own functions share one object
73
77
  { $effects: { fetchJson, race, delay } },
74
78
  );
79
+ ```
80
+
81
+ ```js
82
+ // bus.js
83
+ import { createBus } from "nanoeffects";
84
+ import { scopePlugin } from "nanoeffects/scope";
85
+
86
+ export const bus = createBus().use(scopePlugin());
87
+ ```
88
+
89
+ ```js
90
+ import { bus } from "./bus";
91
+ import { loadFeed } from "./feed";
75
92
 
76
93
  const result = await bus.emit(loadFeed("/feed"));
77
94
  ```
@@ -85,14 +102,27 @@ const result = await bus.emit(loadFeed("/feed"));
85
102
  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.
86
103
 
87
104
  ```js
105
+ // bus.js
88
106
  import { createBus } from "nanoeffects";
89
- import { createCommand } from "nanoeffects/actions";
90
107
  import { scopePlugin } from "nanoeffects/scope";
91
108
 
92
- const bus = createBus().use(scopePlugin());
93
- const getUser = createCommand("user/get", (id, { api }) => api.getUser(id), {
94
- api,
95
- });
109
+ export const bus = createBus().use(scopePlugin());
110
+ ```
111
+
112
+ ```js
113
+ // user.js
114
+ import { createCommand } from "nanoeffects/actions";
115
+
116
+ export const getUser = createCommand(
117
+ "user/get",
118
+ (id, { api }) => api.getUser(id),
119
+ { api },
120
+ );
121
+ ```
122
+
123
+ ```js
124
+ import { bus } from "./bus";
125
+ import { getUser } from "./user";
96
126
 
97
127
  // the real api for this call
98
128
  await bus.emit(getUser("1"));
@@ -112,23 +142,30 @@ Rules for the second argument:
112
142
  ## Give an action, a job or a module its own fields
113
143
 
114
144
  ```js
115
- import { createAction, createJob, createModule } from "nanoeffects/actions";
145
+ // user.js
146
+ import { createAction, createJob } from "nanoeffects/actions";
116
147
 
117
148
  // createAction — the listeners of this action's calls get the fields
118
- const userLoaded = createAction("user/loaded", { store });
149
+ export const userLoaded = createAction("user/loaded", { store });
119
150
 
120
151
  // createJob — the scope is the fourth argument, after the outcomes;
121
152
  // the handler and the listeners of the outcome get its fields
122
- const loadUser = createJob(
153
+ export const loadUser = createJob(
123
154
  "user/load",
124
155
  fetchUser,
125
156
  { success: userLoaded, fail: "user/failed" },
126
157
  { logger },
127
158
  );
159
+ ```
160
+
161
+ ```js
162
+ // audit-module.js
163
+ import { createModule } from "nanoeffects/actions";
164
+ import { userLoaded } from "./user";
128
165
 
129
166
  // createModule — third argument; everything the module owns gets the fields:
130
167
  // m.on listeners, m.handle handlers, m.spawn tasks
131
- const auditModule = createModule(
168
+ export const auditModule = createModule(
132
169
  "audit",
133
170
  (m) => {
134
171
  m.on(userLoaded, (user, { logger }) => logger.info(user.id));
@@ -161,13 +198,14 @@ In `$effects` a function loses its first parameter, and TypeScript cannot keep a
161
198
  `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.
162
199
 
163
200
  ```js
201
+ // pick.js
164
202
  import { createCommand } from "nanoeffects/actions";
165
203
  import { effect } from "nanoeffects/scope";
166
204
 
167
205
  // effect — marks the function, so a scope binds it to the task; the editor keeps its generic
168
206
  const first = effect((ctx, items) => items[0]);
169
207
 
170
- const pick = createCommand("pick", (items, { first }) => first(items), {
208
+ export const pick = createCommand("pick", (items, { first }) => first(items), {
171
209
  first, // a plain key of the scope, not an entry of `$effects`
172
210
  });
173
211
  ```