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.
- package/README.md +2 -2
- package/dist/{action-creator-Bd4TBZgl.d.ts → action-creator-BrZF6hXc.d.ts} +0 -4
- package/dist/actions.d.ts +41 -25
- package/dist/actions.js +1 -1
- package/dist/bus-CkTHUAvq.js +1 -0
- package/dist/context-COgyNlrC.js +1 -0
- package/dist/{context-plugin-BVNKqkIx.d.ts → context-plugin--HkNN1ha.d.ts} +1 -1
- package/dist/dev.d.ts +9 -8
- package/dist/dev.js +1 -1
- package/dist/effects.d.ts +2 -2
- package/dist/effects.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/match.d.ts +50 -0
- package/dist/match.js +1 -0
- package/dist/{module-DZ5ngUrM.d.ts → module-DqKegXHt.d.ts} +43 -18
- package/dist/notrace/{action-creator-D-y_-xRS.d.ts → action-creator-DHAEgwuh.d.ts} +0 -4
- package/dist/notrace/actions.d.ts +90 -39
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/bus-BmCTo8BG.js +1 -0
- package/dist/notrace/{context-plugin-DDfsaErA.d.ts → context-plugin-BapQkGwQ.d.ts} +1 -1
- package/dist/notrace/context-yYU3_9lq.js +1 -0
- package/dist/notrace/{delay-B_W1tnkS.js → delay-BosJKFjg.js} +1 -1
- package/dist/notrace/effects.d.ts +2 -2
- package/dist/notrace/effects.js +1 -1
- package/dist/notrace/index.d.ts +2 -2
- package/dist/notrace/index.js +1 -1
- package/dist/notrace/{plugin-CDAngHwg.d.ts → plugin-Cxadz6uU.d.ts} +2 -2
- package/dist/notrace/policies.d.ts +30 -2
- package/dist/notrace/policies.js +1 -1
- package/dist/notrace/redux.d.ts +1 -1
- package/dist/observable.d.ts +1 -1
- package/dist/oop.d.ts +131 -0
- package/dist/oop.js +1 -0
- package/dist/{plugin-DQNPBcxx.d.ts → plugin-DkXCvsmB.d.ts} +2 -2
- package/dist/policies.d.ts +3 -2
- package/dist/policies.js +1 -1
- package/dist/react.d.ts +33 -1
- package/dist/react.js +1 -1
- package/dist/redux.d.ts +1 -1
- package/dist/scope.d.ts +5 -6
- package/dist/scope.js +1 -1
- package/dist/signal.d.ts +51 -0
- package/dist/signal.js +1 -0
- package/dist/trace.d.ts +1 -1
- package/dist/{types-BJ0fzV1p.d.ts → types-KavbvHix.d.ts} +13 -3
- package/dist/with-policy-CqUS-VWW.d.ts +30 -0
- package/dist/with-policy-DBV9dh2F.js +1 -0
- package/docs/README.md +7 -0
- package/docs/actions.md +152 -39
- package/docs/cancel-stale-requests.md +111 -0
- package/docs/debounce-and-abort.md +106 -0
- package/docs/guide.md +143 -19
- package/docs/hmr.md +6 -5
- package/docs/migrating-from-effector.md +51 -18
- package/docs/migrating-from-mitt.md +97 -0
- package/docs/migrating-from-rxjs.md +17 -6
- package/docs/motivation.md +41 -0
- package/docs/observable.md +124 -10
- package/docs/oop.md +286 -0
- package/docs/package.md +6 -2
- package/docs/plugins.md +247 -51
- package/docs/policies.md +33 -0
- package/docs/prevent-double-submit.md +128 -0
- package/docs/query-libraries.md +124 -0
- package/docs/react.md +61 -5
- package/docs/scope.md +60 -22
- package/docs/stores.md +263 -109
- package/llms-full.txt +2153 -355
- package/llms.txt +10 -3
- package/package.json +43 -4
- package/skills/nanoeffects/SKILL.md +52 -0
- package/dist/bus-BAs9x7IQ.js +0 -1
- package/dist/context-CK0ZomWt.js +0 -1
- package/dist/notrace/bus-Bdk0YyFQ.js +0 -1
- 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
|
-
|
|
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
|
-
//
|
|
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 "./
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
```
|