nanoeffects 0.28.0 → 0.30.1

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 (139) hide show
  1. package/README.md +6 -5
  2. package/dist/{action-creator-CcAEvUnw.d.ts → action-creator-6rlBTmH9.d.ts} +60 -33
  3. package/dist/actions.d.ts +18 -18
  4. package/dist/actions.js +1 -1
  5. package/dist/bus-DFlmNJ6h.js +1 -0
  6. package/dist/context-B5Z2RpJt.js +1 -0
  7. package/dist/context-types-DKNc5mkS.d.ts +31 -0
  8. package/dist/context.d.ts +8 -3
  9. package/dist/context.js +1 -1
  10. package/dist/{delay-18LJ8jSk.js → delay-5OhJZZQ0.js} +1 -1
  11. package/dist/dev.d.ts +44 -3
  12. package/dist/dev.js +2 -1
  13. package/dist/effects.d.ts +2 -2
  14. package/dist/effects.js +1 -1
  15. package/dist/emit-key-C6FL7Ewn.js +1 -0
  16. package/dist/guard.d.ts +59 -0
  17. package/dist/guard.js +1 -0
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.js +1 -1
  20. package/dist/match.d.ts +3 -3
  21. package/dist/match.js +1 -1
  22. package/dist/{module-C_oEXw1n.d.ts → module-G39MaVOt.d.ts} +38 -38
  23. package/dist/notrace/{action-creator-DxOvVo8I.d.ts → action-creator-BrMSO3RP.d.ts} +35 -32
  24. package/dist/notrace/actions.d.ts +52 -52
  25. package/dist/notrace/actions.js +1 -1
  26. package/dist/notrace/{bus-DmKn4SDD.js → bus-37nlVljc.js} +1 -1
  27. package/dist/notrace/effects.d.ts +2 -2
  28. package/dist/notrace/index.d.ts +2 -2
  29. package/dist/notrace/index.js +1 -1
  30. package/dist/notrace/{plugin-lJjjd-SN.d.ts → plugin-7GUL2EW5.d.ts} +1 -1
  31. package/dist/notrace/policies.d.ts +1 -1
  32. package/dist/notrace/redux.d.ts +1 -1
  33. package/dist/observable.d.ts +43 -1
  34. package/dist/observable.js +1 -1
  35. package/dist/onAbort-BgMfspK4.js +1 -0
  36. package/dist/oop.d.ts +3 -3
  37. package/dist/oop.js +1 -1
  38. package/dist/{plugin-CuvIgjnX.d.ts → plugin-BWNP23hr.d.ts} +2 -2
  39. package/dist/policies.d.ts +2 -2
  40. package/dist/policies.js +1 -1
  41. package/dist/production/dev.js +1 -1
  42. package/dist/react.d.ts +2 -2
  43. package/dist/react.js +1 -1
  44. package/dist/redux.d.ts +1 -1
  45. package/dist/scope.d.ts +9 -9
  46. package/dist/signal-field-key-CWT-3OXC.js +1 -0
  47. package/dist/signal.d.ts +34 -6
  48. package/dist/signal.js +1 -1
  49. package/dist/site-DLvUoXs3.js +1 -0
  50. package/dist/svelte.d.ts +1 -1
  51. package/dist/trace.d.ts +170 -6
  52. package/dist/trace.js +3 -1
  53. package/dist/{types-FV8fuvI9.d.ts → types-N_cdRr1r.d.ts} +2 -2
  54. package/dist/vue.d.ts +1 -1
  55. package/dist/{with-policy-D-SQRkPh.js → with-policy-CeM1rtES.js} +1 -1
  56. package/dist/{with-policy-3bhFLEXs.d.ts → with-policy-CthiBp_z.d.ts} +1 -1
  57. package/docs/README.md +65 -22
  58. package/docs/actions.md +56 -13
  59. package/docs/cancel-stale-requests.md +2 -0
  60. package/docs/chat-example.md +15 -1
  61. package/docs/chat-widget.md +9 -7
  62. package/docs/context.md +352 -8
  63. package/docs/debounce-and-abort.md +3 -1
  64. package/docs/debugging-loops.md +283 -0
  65. package/docs/effects.md +259 -131
  66. package/docs/errors.md +4 -2
  67. package/docs/examples.md +60 -0
  68. package/docs/guide.md +57 -22
  69. package/docs/hmr.md +35 -1
  70. package/docs/loop-guard.md +109 -0
  71. package/docs/migrating-from-effector.md +15 -2
  72. package/docs/migrating-from-mitt.md +6 -4
  73. package/docs/migrating-from-rxjs.md +2 -0
  74. package/docs/migrating-from-saga.md +2 -0
  75. package/docs/motivation.md +14 -21
  76. package/docs/observable.md +45 -0
  77. package/docs/oop.md +20 -1
  78. package/docs/package.md +25 -13
  79. package/docs/pass-the-signal.md +281 -0
  80. package/docs/plugins.md +90 -15
  81. package/docs/policies.md +73 -46
  82. package/docs/prevent-double-submit.md +3 -1
  83. package/docs/query-libraries.md +276 -6
  84. package/docs/react.md +50 -11
  85. package/docs/recipes.md +528 -0
  86. package/docs/ru/README.md +62 -0
  87. package/docs/ru/actions.md +412 -0
  88. package/docs/ru/cancel-stale-requests.md +106 -0
  89. package/docs/ru/chat-example.md +259 -0
  90. package/docs/ru/chat-widget.md +131 -0
  91. package/docs/ru/context.md +401 -0
  92. package/docs/ru/debounce-and-abort.md +105 -0
  93. package/docs/ru/debugging-loops.md +283 -0
  94. package/docs/ru/effects.md +538 -0
  95. package/docs/ru/errors.md +36 -0
  96. package/docs/ru/examples.md +60 -0
  97. package/docs/ru/guide.md +530 -0
  98. package/docs/ru/hmr.md +83 -0
  99. package/docs/ru/loop-guard.md +109 -0
  100. package/docs/ru/migrating-from-effector.md +339 -0
  101. package/docs/ru/migrating-from-mitt.md +99 -0
  102. package/docs/ru/migrating-from-rxjs.md +197 -0
  103. package/docs/ru/migrating-from-saga.md +97 -0
  104. package/docs/ru/motivation.md +47 -0
  105. package/docs/ru/observable.md +233 -0
  106. package/docs/ru/oop.md +431 -0
  107. package/docs/ru/package.md +42 -0
  108. package/docs/ru/pass-the-signal.md +281 -0
  109. package/docs/ru/plugins.md +398 -0
  110. package/docs/ru/policies.md +201 -0
  111. package/docs/ru/prevent-double-submit.md +122 -0
  112. package/docs/ru/query-libraries.md +392 -0
  113. package/docs/ru/react.md +309 -0
  114. package/docs/ru/recipes.md +528 -0
  115. package/docs/ru/semantics.md +19 -0
  116. package/docs/ru/server.md +204 -0
  117. package/docs/ru/starter.md +118 -0
  118. package/docs/ru/stores.md +673 -0
  119. package/docs/ru/svelte.md +78 -0
  120. package/docs/ru/tracing.md +466 -0
  121. package/docs/ru/typescript.md +170 -0
  122. package/docs/ru/vue.md +92 -0
  123. package/docs/semantics.md +12 -0
  124. package/docs/server.md +5 -3
  125. package/docs/starter.md +2 -0
  126. package/docs/stores.md +2 -0
  127. package/docs/svelte.md +3 -1
  128. package/docs/tracing.md +132 -7
  129. package/docs/typescript.md +3 -1
  130. package/docs/vue.md +3 -1
  131. package/llms-full.txt +2718 -545
  132. package/llms.txt +17 -14
  133. package/package.json +25 -8
  134. package/skills/nanoeffects/SKILL.md +2 -2
  135. package/dist/bus-DlpEluzd.js +0 -1
  136. package/dist/context-UcmrAIhP.js +0 -1
  137. package/dist/context-types-hCKgU8sC.d.ts +0 -14
  138. package/docs/scope.md +0 -266
  139. package/docs/stability.md +0 -90
package/docs/policies.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # Policies
2
2
 
3
+ > **Level B.** Read first: [Guide](guide.md#1-create-a-bus) steps 1 to 3.
4
+
3
5
  Use this page when handlers overlap in a SPA (search-as-you-type, double submit, leave page). A **policy** wraps a handler and decides what happens then.
4
6
 
5
7
  Start with `latest`. The table and sections below are the rest of the reference.
6
8
 
7
- ## Abort the previous run
9
+ ## `latest`: abort the previous run
8
10
 
9
11
  ```js
10
12
  import { createBus } from "nanoeffects";
@@ -26,7 +28,7 @@ Pass `signal` to `fetch` (and the like) so the request cancels.
26
28
 
27
29
  ## Choose a policy
28
30
 
29
- Every policy accepts `key` in its options (see [Isolate state by key](#isolate-state-by-key)).
31
+ Every policy accepts `key` in its options (see [`key`: isolate state by key](#key-isolate-state-by-key)).
30
32
 
31
33
  | Policy | Overlapping calls |
32
34
  | ------ | ----------------- |
@@ -36,54 +38,79 @@ Every policy accepts `key` in its options (see [Isolate state by key](#isolate-s
36
38
  | `throttle(ms, fn, options?)` | at most one run per `ms` |
37
39
  | `queue(fn, options?)` | one after another, in order |
38
40
 
39
- ## Call a policy without its function
41
+ Each policy has a short entry in [Look up a policy](#look-up-a-policy).
40
42
 
41
- **Problem.** Some code builds the function for you and only asks which policy to use, as [`usePolicy`](react.md#keep-a-debounced-handler-between-renders) does. If you must pass your own function there, it is easy to pass the wrong one.
43
+ ## Look up a policy
42
44
 
43
- **When.** You pass a policy to `usePolicy`. With a bus, `withPolicy` or your own code, call the policy with its function as above.
45
+ One entry per policy: what it does, and a link to the full recipe.
44
46
 
45
- 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:
47
+ ### `latest`
46
48
 
47
- ```js
48
- import { debounce } from "nanoeffects/policies";
49
+ A newer call aborts the older one and drops its result.
49
50
 
50
- const runSearch = async (query) => {
51
- await api.search(query);
52
- };
51
+ Signature: `latest(fn, options?)`.
53
52
 
54
- // debounce(300) — no function yet: returns a wrapper
55
- const wrap = debounce(300);
53
+ More: [`latest`: abort the previous run](#latest-abort-the-previous-run).
56
54
 
57
- // the wrapper takes the function; same as debounce(300, runSearch)
58
- const search = wrap(runSearch);
59
- ```
55
+ ### `leading`
56
+
57
+ New calls are dropped while a run is going.
58
+
59
+ Signature: `leading(fn, options?)`.
60
+
61
+ More: [Prevent a double submit](prevent-double-submit.md).
62
+
63
+ ### `debounce`
64
+
65
+ Runs after a period of quiet.
66
+
67
+ Signature: `debounce(ms, fn, options?)`.
68
+
69
+ More: [Configure debounce and throttle](#configure-debounce-and-throttle).
70
+
71
+ ### `throttle`
72
+
73
+ At most one run within `ms`.
60
74
 
61
- - 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 })`.
62
- - Each time you apply a wrapper to a function, you get a new policy with its own state.
63
- - To combine policies, nest them as before: `throttle(100, latest(fn))`.
64
- - 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.
75
+ Signature: `throttle(ms, fn, options?)`.
76
+
77
+ More: [Configure debounce and throttle](#configure-debounce-and-throttle).
78
+
79
+ ### `queue`
80
+
81
+ Builds a queue of calls: they run one after another, in order.
82
+
83
+ Signature: `queue(fn, options?)`.
84
+
85
+ More: [Run calls one after another with `queue`](#run-calls-one-after-another-with-queue).
86
+
87
+ ### `key`
88
+
89
+ An option of every policy. It splits the state of the policy by a property, for example by id: calls with different keys do not affect each other.
90
+
91
+ Signature: `latest(fn, { key: (payload) => payload.id })`.
92
+
93
+ More: [`key`: isolate state by key](#key-isolate-state-by-key).
94
+
95
+ To use a policy for your own function with no bus, see [`withPolicy`: call a policy outside a bus](#withpolicy-call-a-policy-outside-a-bus).
65
96
 
66
97
  ## Configure debounce and throttle
67
98
 
68
- `debounce` and `throttle` take `leading` and `trailing`. Behavior matches lodash (and patronum); tests compare random call sequences to lodash itself.
99
+ `debounce` and `throttle` take two options, `leading` and `trailing`. They decide which call of a series runs. `leading` runs the first call of the series at once. `trailing` runs the last call at the end of the series. Both functions are built on the same algorithm as in lodash (and patronum) and behave exactly like them.
69
100
 
70
- **Throttle** (`throttle(ms, fn)`):
101
+ **Throttle** (`throttle(ms, fn)`) runs a call at once if it has been idle for at least `ms`, holds the calls that come within `ms`, and runs the last of them when the window closes. Under a steady stream it runs every `ms`.
71
102
 
72
- - Default `leading: true`, `trailing: true`.
73
- - A call when the throttle is idle (at least `ms` after the previous call and run) runs at once.
74
- - Calls made while it waits are held; the latest runs when the window closes.
75
- - Under a steady stream it runs every `ms`.
76
- - `leading: false` — even the first call waits for the end of its window.
77
- - `trailing: false` — drop calls made during a window.
103
+ - `leading` — run the first call of a series at once. It is `true` by default. With `false`, even the first call waits for the end of its window.
104
+ - `trailing` — run the last held call when the window closes. It is `true` by default. With `false`, calls made during a window are dropped.
78
105
 
79
- **Debounce** (`debounce(ms, fn)`):
106
+ **Debounce** (`debounce(ms, fn)`) runs the last call of a series after `ms` of quiet.
80
107
 
81
- - Default `trailing: true` — run the last call after `ms` of quiet.
82
- - `leading: true` — also run the first call of a burst at once; the trailing run happens only if more calls follow.
83
- - `leading: true, trailing: false` — only the first call of a burst.
108
+ - `trailing` — run the last call after `ms` of quiet. It is `true` by default.
109
+ - `leading` — also run the first call of a series at once. It is `false` by default. The closing run then happens only if more calls follow.
110
+ - `leading: true, trailing: false` — only the first call of a series.
84
111
  - Both `false` — nothing runs.
85
112
 
86
- ## Isolate state by key
113
+ ## `key`: isolate state by key
87
114
 
88
115
  `key` gives each key its own state. Calls with different keys do not affect each other. Idle keys are dropped, so keys can be unbounded (ids, urls).
89
116
 
@@ -142,35 +169,35 @@ Every call returns a promise with the result of its own run. An error rejects on
142
169
  - When a running call aborts, its slot is freed at once and its result is dropped.
143
170
  - Pass `signal` into what `fn` does. A `fn` that ignores it can overlap with the next run.
144
171
 
145
- ## Use a policy without a bus
172
+ ## `withPolicy`: call a policy outside a bus
146
173
 
147
- **Problem.** You want a debounced input handler or a "latest request wins" fetch, but your code has no bus. A policy needs a context on every call, so `debounce(300, search)("re")` does not work on its own.
174
+ **Problem.** You want a debounced input handler or a "latest request wins" fetch for your own function, but the code has no bus: a DOM handler, a click callback, a function in a service. A policy needs a context on every call, so `debounce(300, search)("re")` does not work on its own.
148
175
 
149
- **When.** One component or module needs a policy and nothing else from the library. If you already have a bus, `bus.on(event, policy)` is simpler.
176
+ **When.** One input, button or function needs a policy and nothing else from the library. If you already have a bus, `bus.on(event, policy)` is simpler. In a React component use [`usePolicy`](react.md#keep-a-debounced-handler-between-renders), which also keeps the function between renders.
150
177
 
151
- `withPolicy` takes the result of any policy and returns a plain function. It needs no bus and no context argument, and it keeps the state of the policy between calls.
178
+ `withPolicy` takes a policy and returns a plain function. The function needs no bus and no context argument, and it keeps the state of the policy between calls.
152
179
 
153
180
  ```js
154
181
  import { debounce, withPolicy } from "nanoeffects/policies";
155
182
 
183
+ const input = document.querySelector("#search");
184
+
156
185
  // debounce — waits for 300 ms of quiet, then runs the callback
157
186
  const policy = debounce(300, async (query, { signal }) => {
158
- await api.search(query, { signal });
187
+ const items = await api.search(query, { signal });
188
+ render(items);
159
189
  });
160
190
 
161
191
  // withPolicy — a plain function: no bus, no context argument
162
- const onChange = withPolicy(policy);
192
+ const onInput = withPolicy(policy);
163
193
 
164
- onChange("re");
165
- onChange("redux"); // only this call runs, 300 ms after it
194
+ input.addEventListener("input", (event) => onInput(event.target.value));
166
195
 
167
- // cancel — drops a pending call and aborts a running one
168
- onChange.cancel();
196
+ // cancel — drops a pending call and aborts a running request, for example when the page closes
197
+ window.addEventListener("pagehide", () => onInput.cancel());
169
198
  ```
170
199
 
171
200
  - The callback has the same `(payload, ctx)` shape as for a bus. `ctx.signal`, `ctx.abort` and `ctx.run` work. `ctx.emit` and `ctx.on` do nothing, because there is no bus.
172
201
  - Every call returns a promise with the result. A call the policy drops resolves with `undefined`.
173
202
  - `cancel()` aborts every call that has not finished. Calls made after it work again.
174
- - Where `Symbol.dispose` exists, `using onChange = withPolicy(policy)` calls `cancel()` at the end of the scope.
175
-
176
- In a React component, use [`usePolicy`](react.md#keep-a-debounced-handler-between-renders) instead: it keeps the function between renders.
203
+ - Where `Symbol.dispose` exists, `using onInput = withPolicy(policy)` calls `cancel()` at the end of the scope.
@@ -1,5 +1,7 @@
1
1
  # Prevent a double submit
2
2
 
3
+ > **Level B.** Read first: [Policies](policies.md).
4
+
3
5
  Read this page if a second click on "Pay", "Sign up" or "Save" sends the request twice. It shows how to ignore the second click while the first request runs, how to do it per item, and when to queue the calls instead.
4
6
 
5
7
  ## See the problem
@@ -94,7 +96,7 @@ bus.on(
94
96
  );
95
97
  ```
96
98
 
97
- Backoff between tries: [Back off between retries](effects.md#back-off-between-retries).
99
+ Backoff between tries: [`backoff`](effects.md#backoff).
98
100
 
99
101
  ## Guard a handler when you have no bus
100
102
 
@@ -1,8 +1,23 @@
1
- # Use nanoeffects with React Query and SWR
1
+ # Use nanoeffects with React Query, SWR and RTK Query
2
2
 
3
- Read this page if you fetch data with [React Query](https://tanstack.com/query) (TanStack Query) or [SWR](https://swr.vercel.app) and want `retry`, `timeout` and the abort of nanoeffects inside the request. It shows one action that both libraries call.
3
+ > **Level E.** Read first: [Guide](guide.md#1-create-a-bus) steps 1 to 3.
4
4
 
5
- You need a React project, `nanoeffects`, and one of the two libraries. `api` below is your own client: replace it with your requests.
5
+ Read this page if you fetch data with [React Query](https://tanstack.com/query) (TanStack Query), [SWR](https://swr.vercel.app) or [RTK Query](https://redux-toolkit.js.org/rtk-query/overview) and want `retry`, `timeout` and the abort of nanoeffects inside the request. It shows one action that the libraries call, then three recipes for the parts around it: a client that stops with the request, a live stream into the cache, and a refetch on a bus event.
6
+
7
+ You need a React project, `nanoeffects`, and one of the libraries. `api` below is your own client: replace it with your requests.
8
+
9
+ ## On this page
10
+
11
+ - [Decide who owns what](#decide-who-owns-what)
12
+ - [Make the action](#make-the-action)
13
+ - [Call it from React Query](#call-it-from-react-query)
14
+ - [Call it from SWR](#call-it-from-swr)
15
+ - [Let the bus know the result](#let-the-bus-know-the-result)
16
+ - [Call it from RTK Query](#call-it-from-rtk-query)
17
+ - [Give the client its signal once](#give-the-client-its-signal-once)
18
+ - [Push a live stream into the cache](#push-a-live-stream-into-the-cache)
19
+ - [Refetch queries when the bus says so](#refetch-queries-when-the-bus-says-so)
20
+ - [Know the limits](#know-the-limits)
6
21
 
7
22
  ## Decide who owns what
8
23
 
@@ -63,7 +78,7 @@ import { getUser } from "./user";
63
78
  export function Profile({ id }) {
64
79
  const { isLoading, data, error } = useQuery({
65
80
  queryKey: ["user", id],
66
- // emit — runs the command; { signal } ties its task to the signal of React Query
81
+ // emit — runs the action; { signal } ties its task to the signal of React Query
67
82
  queryFn: ({ signal }) => bus.emit(getUser(id), { signal }),
68
83
  });
69
84
 
@@ -113,11 +128,266 @@ import { createAction } from "nanoeffects/actions";
113
128
  export const userLoaded = createAction("user/loaded");
114
129
  ```
115
130
 
116
- Send it from the handler with `ctx.emit(userLoaded(user))`. Do not replace the action with `createJob` here: a job handles the error itself and resolves with `undefined`, so React Query would never see the failure. Use a job when nothing waits for the result and the app shows a loading state from the bus, see [Show a loading state with a job](actions.md#show-a-loading-state-with-a-job).
131
+ Send it from the handler with `ctx.emit(userLoaded(user))`. Do not replace the action with `createJob` here: a job handles the error itself and resolves with `undefined`, so React Query would never see the failure. Use a job when nothing waits for the result and the app shows a loading state from the bus, see [Show a loading state with a job](actions.md#the-state-of-a-job).
132
+
133
+ ## Call it from RTK Query
134
+
135
+ <!-- docs-check: new -->
136
+
137
+ RTK Query gives the `queryFn` of an endpoint a `signal` in its second argument, so the call is the same as with React Query: pass it to `emit`. This page does not set up the store; see [Stores](stores.md) for that.
138
+
139
+ ```js
140
+ // bus.js
141
+ import { createBus } from "nanoeffects";
142
+ import { signalPlugin } from "nanoeffects/signal";
143
+
144
+ // signalPlugin — lets emit take { signal }
145
+ export const bus = createBus().use(signalPlugin());
146
+ ```
147
+
148
+ ```js
149
+ // user.js
150
+ import { createAction } from "nanoeffects/actions";
151
+
152
+ // createAction with a handler — emit returns what the handler returns
153
+ export const getUser = createAction("user/get", (id, { signal }) =>
154
+ api.getUser(id, { signal }),
155
+ );
156
+ ```
157
+
158
+ ```js
159
+ // api-slice.js
160
+ import { createApi, fakeBaseQuery } from "@reduxjs/toolkit/query/react";
161
+ import { bus } from "./bus";
162
+ import { getUser } from "./user";
163
+
164
+ export const userApi = createApi({
165
+ reducerPath: "userApi",
166
+ baseQuery: fakeBaseQuery(), // RTK Query — every endpoint brings its own queryFn
167
+ endpoints: (build) => ({
168
+ getUser: build.query({
169
+ // emit — runs the action; { signal } ties its task to the request of RTK Query
170
+ queryFn: async (id, { signal }) => {
171
+ const user = await bus.emit(getUser(id), { signal });
172
+ return { data: user };
173
+ },
174
+ }),
175
+ }),
176
+ });
177
+ ```
178
+
179
+ ## Give the client its signal once
180
+
181
+ <!-- docs-check: new -->
182
+
183
+ Problem: the action passes `signal` to every call of `api`, and one forgotten `{ signal }` leaves a request running after the query is gone. `fromSignal` builds the client from the signal of each task, so the handler cannot forget it. Use it when many actions share one client. A single action is simpler as it is in [Make the action](#make-the-action).
184
+
185
+ `fromSignal` marks a field of `contextPlugin`. The client is made when a handler first reads `ctx.http`, from the `ctx.signal` of that task. More in [Context fields](context.md#build-a-field-from-the-signal-of-each-task).
186
+
187
+ ```js
188
+ // bus.js
189
+ import axios from "axios";
190
+ import { createBus } from "nanoeffects";
191
+ import { contextPlugin } from "nanoeffects/context";
192
+ import { fromSignal, signalPlugin } from "nanoeffects/signal";
193
+
194
+ // fromSignal — http is a new axios client for each task, bound to the signal of that task
195
+ const http = fromSignal((signal) => axios.create({ baseURL: "/api", signal }));
196
+
197
+ // contextPlugin — every handler gets ctx.http
198
+ const context = contextPlugin({ http });
199
+
200
+ // signalPlugin — emit takes { signal }, so the signal of the query reaches the task
201
+ export const bus = createBus().use(signalPlugin()).use(context);
202
+ ```
203
+
204
+ ```js
205
+ // user.js
206
+ import { createAction } from "nanoeffects/actions";
207
+
208
+ // the handler never sees a signal: ctx.http stops when the task aborts
209
+ export const getUser = createAction("user/get", async (id, { http }) => {
210
+ const response = await http.get(`/users/${id}`);
211
+ return response.data;
212
+ });
213
+ ```
214
+
215
+ ```jsx
216
+ // Profile.jsx
217
+ import { useQuery } from "@tanstack/react-query";
218
+ import { bus } from "./bus";
219
+ import { getUser } from "./user";
220
+
221
+ export function Profile({ id }) {
222
+ const { data } = useQuery({
223
+ queryKey: ["user", id],
224
+ // emit — the signal of React Query aborts the task, and with it the request of ctx.http
225
+ queryFn: ({ signal }) => bus.emit(getUser(id), { signal }),
226
+ });
227
+
228
+ return <p>{data?.name}</p>;
229
+ }
230
+ ```
231
+
232
+ ## Push a live stream into the cache
233
+
234
+ <!-- docs-check: new -->
235
+
236
+ Problem: prices arrive from a socket, and the component should show the newest one without a refetch. The socket code emits an event on the bus; the component reads those events with `events` and writes each into the cache. Both libraries have an entry point for pushed data: `setQueryData` in React Query and `useSWRSubscription` in SWR.
237
+
238
+ `events` gives the payloads to a `for await` loop. `buffer: 1` keeps only the newest price while a write is running. The abort in the cleanup ends the loop and removes the listener. More in [Read an event with a `for await` loop](observable.md#read-an-event-with-a-for-await-loop).
239
+
240
+ ```js
241
+ // bus.js
242
+ import { createBus } from "nanoeffects";
243
+
244
+ // createBus — the bus the socket code and the components share
245
+ export const bus = createBus();
246
+ ```
247
+
248
+ ```js
249
+ // prices.js
250
+ import { createAction } from "nanoeffects/actions";
251
+
252
+ // createAction — the event the socket code emits for every new price
253
+ export const priceChanged = createAction("price/changed");
254
+ ```
255
+
256
+ ```jsx
257
+ // Price.jsx
258
+ import { useEffect } from "react";
259
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
260
+ import { events } from "nanoeffects/observable";
261
+ import { bus } from "./bus";
262
+ import { priceChanged } from "./prices";
263
+
264
+ export function Price({ symbol }) {
265
+ const queryClient = useQueryClient();
266
+ const { data } = useQuery({
267
+ queryKey: ["price", symbol],
268
+ queryFn: () => api.getPrice(symbol), // the first value; the stream keeps it fresh
269
+ });
270
+
271
+ useEffect(() => {
272
+ const controller = new AbortController();
273
+ // events — the newest price event, one at a time
274
+ const prices = events(bus, priceChanged, {
275
+ signal: controller.signal,
276
+ buffer: 1,
277
+ });
278
+
279
+ async function follow() {
280
+ for await (const price of prices) {
281
+ if (price.symbol === symbol) {
282
+ queryClient.setQueryData(["price", symbol], price.value); // write into the cache
283
+ }
284
+ }
285
+ }
286
+
287
+ follow().catch((error) => {
288
+ if (!controller.signal.aborted) throw error; // the abort ends the loop; anything else is a bug
289
+ });
290
+
291
+ return () => controller.abort(); // the component left: the loop ends, the listener goes
292
+ }, [queryClient, symbol]);
293
+
294
+ return <p>{data}</p>;
295
+ }
296
+ ```
297
+
298
+ SWR does the same with `useSWRSubscription` from `swr/subscription`. Its subscribe function gets `next` to push a value and returns the cleanup:
299
+
300
+ ```jsx
301
+ // Price.jsx
302
+ import useSWRSubscription from "swr/subscription";
303
+ import { events } from "nanoeffects/observable";
304
+ import { bus } from "./bus";
305
+ import { priceChanged } from "./prices";
306
+
307
+ export function Price({ symbol }) {
308
+ const { data } = useSWRSubscription(["price", symbol], (_key, { next }) => {
309
+ const controller = new AbortController();
310
+ // events — the newest price event, one at a time
311
+ const prices = events(bus, priceChanged, {
312
+ signal: controller.signal,
313
+ buffer: 1,
314
+ });
315
+
316
+ async function follow() {
317
+ for await (const price of prices) {
318
+ if (price.symbol === symbol) {
319
+ next(null, price.value); // SWR — push the value to the hook
320
+ }
321
+ }
322
+ }
323
+
324
+ follow().catch((error) => {
325
+ if (!controller.signal.aborted) next(error);
326
+ });
327
+
328
+ return () => controller.abort(); // SWR calls this when the last component of the key leaves
329
+ });
330
+
331
+ return <p>{data}</p>;
332
+ }
333
+ ```
334
+
335
+ ## Refetch queries when the bus says so
336
+
337
+ <!-- docs-check: new -->
338
+
339
+ Problem: a save in one part of the app should refresh the lists in another. The save emits an event, and the component that owns the queries listens for it. One `AbortController` ends every listener of the component, instead of one `unbind` for each. For a single listener, `return bus.on(…)` from the effect is shorter.
340
+
341
+ This needs `signalPlugin`, because `{ signal }` in the options of `on` is read by the plugin. Without it the listeners stay after the component leaves.
342
+
343
+ ```js
344
+ // bus.js
345
+ import { createBus } from "nanoeffects";
346
+ import { signalPlugin } from "nanoeffects/signal";
347
+
348
+ // signalPlugin — on takes { signal } and removes the listener when it aborts
349
+ export const bus = createBus().use(signalPlugin());
350
+ ```
351
+
352
+ ```js
353
+ // user-events.js
354
+ import { createAction } from "nanoeffects/actions";
355
+
356
+ // createAction — events the save code emits
357
+ export const userSaved = createAction("user/saved");
358
+ export const userDeleted = createAction("user/deleted");
359
+ ```
360
+
361
+ ```jsx
362
+ // UserList.jsx
363
+ import { useEffect } from "react";
364
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
365
+ import { bus } from "./bus";
366
+ import { userDeleted, userSaved } from "./user-events";
367
+
368
+ export function UserList() {
369
+ const queryClient = useQueryClient();
370
+ const { data } = useQuery({ queryKey: ["users"], queryFn: () => api.listUsers() });
371
+
372
+ useEffect(() => {
373
+ const controller = new AbortController();
374
+ const { signal } = controller;
375
+ const refresh = () => queryClient.invalidateQueries({ queryKey: ["users"] });
376
+
377
+ // on with { signal } — both listeners go when the component leaves
378
+ bus.on(userSaved, refresh, { signal });
379
+ bus.on(userDeleted, refresh, { signal });
380
+
381
+ return () => controller.abort();
382
+ }, [queryClient]);
383
+
384
+ return <ul>{data?.map((user) => <li key={user.id}>{user.name}</li>)}</ul>;
385
+ }
386
+ ```
117
387
 
118
388
  ## Know the limits
119
389
 
120
- - **Without the plugin, `{ signal }` is ignored.** The action runs, but the abort does not reach it. Call `bus.use(signalPlugin())` once, where the bus is made.
390
+ - **Without the plugin, `{ signal }` is ignored,** in `emit` and in the options of `on`. The action runs, but the abort does not reach it. Call `bus.use(signalPlugin())` once, where the bus is made.
121
391
  - **Only the call forms take it.** `bus.emit(getUser(id), { signal })` works; `bus.emit("user/get", id, { signal })` does not.
122
392
  - **A handler that returns without a promise is finished with the call**, so a signal that aborts later does not reach it. Actions that fetch are async, so this only matters for a sync handler that sets up a callback.
123
393
  - **Give each request its own signal for an event.** For an action with a handler or a job the link ends when it settles. For `bus.emit(action, { signal })` it stays until the signal aborts.
package/docs/react.md CHANGED
@@ -1,20 +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
- ## See what goes wrong without a bus
11
+ ## On this page
12
+
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)
17
+ - [Show the last value of an event as state](#show-the-last-value-of-an-event-as-state)
18
+ - [Keep a debounced handler between renders](#keep-a-debounced-handler-between-renders)
19
+ - [Where to go next](#where-to-go-next)
20
+
21
+ ## Problems of the form without a bus
8
22
 
9
23
  A registration form has three problems that `useEffect` and `fetch` leave to you:
10
24
 
11
- - **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.
12
26
  - **A double submit.** A second click on "Sign up" sends a second request while the first one is running.
13
27
  - **A flaky network.** One failed request should be retried, but a "name is taken" answer should not.
14
28
 
15
29
  Each has a policy or an effect: `latest`, `leading`, `retry`. They live in one place, outside the component.
16
30
 
17
- ## Wire the bus once
31
+ ## 1. The bus and the handlers
18
32
 
19
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.
20
34
 
@@ -71,11 +85,11 @@ bus.on(
71
85
 
72
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`.
73
87
 
74
- ## Connect the form
88
+ ## 2. The form component
75
89
 
76
90
  <!-- docs-check: continue -->
77
91
 
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.
92
+ 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. If the component already holds an `AbortSignal`, `bus.on(event, handler, { signal })` ends the listener with it; see [`signalPlugin`](plugins.md#tie-an-emit-or-a-listener-to-an-outside-signal-signalplugin).
79
93
 
80
94
  ```jsx
81
95
  // SignUpForm.jsx
@@ -142,11 +156,11 @@ The `disabled` button is only for the look. The form stays correct without it: `
142
156
 
143
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.
144
158
 
145
- ## Stop running work when the form closes
159
+ ## 3. Stop requests when the form closes
146
160
 
147
161
  <!-- docs-check: continue -->
148
162
 
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`:
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`:
150
164
 
151
165
  ```js
152
166
  import { latest } from "nanoeffects/policies";
@@ -268,12 +282,37 @@ export const Search = ({ wait }) => {
268
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.
269
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.
270
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.
271
- - 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.
272
311
 
273
312
  ## Where to go next
274
313
 
275
314
  - The policies behind this form: [Policies](policies.md).
276
- - 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).
277
316
  - `retry`, `timeout` and the other effects: [Effects](effects.md).
278
317
  - A state library instead of `useState`: [Stores](stores.md).
279
- - React Query or SWR instead of your own state: [Use nanoeffects with React Query and SWR](query-libraries.md).
318
+ - React Query or SWR instead of your own state: [Use nanoeffects with React Query, SWR and RTK Query](query-libraries.md).