nanoeffects 0.33.0 → 0.35.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 +9 -1
- package/dist/actions.d.ts +2 -2
- package/dist/actions.js +1 -1
- package/dist/dev.d.ts +1 -1
- package/dist/{module-DPKjzfRk.d.ts → module-Fk4OtkZk.d.ts} +10 -27
- package/dist/notrace/actions.d.ts +10 -27
- package/dist/notrace/actions.js +1 -1
- package/dist/signal.d.ts +11 -10
- package/dist/signal.js +1 -1
- package/docs/README.md +29 -2
- package/docs/actions.md +13 -7
- package/docs/cancel-stale-requests.md +1 -1
- package/docs/context.md +1 -1
- package/docs/guide.md +6 -6
- package/docs/package.md +1 -1
- package/docs/pass-the-signal.md +45 -17
- package/docs/policies.md +1 -1
- package/docs/react.md +41 -0
- package/docs/ru/README.md +29 -2
- package/docs/ru/actions.md +13 -7
- package/docs/ru/cancel-stale-requests.md +1 -1
- package/docs/ru/context.md +1 -1
- package/docs/ru/guide.md +6 -6
- package/docs/ru/package.md +1 -1
- package/docs/ru/pass-the-signal.md +46 -18
- package/docs/ru/policies.md +1 -1
- package/docs/ru/react.md +41 -0
- package/docs/ru/svelte.md +41 -0
- package/docs/ru/vue.md +45 -0
- package/docs/svelte.md +41 -0
- package/docs/vue.md +45 -0
- package/llms-full.txt +195 -34
- package/package.json +2 -2
package/docs/react.md
CHANGED
|
@@ -8,6 +8,47 @@ Steps 1 and 2 are required, step 3 is not. The last two sections are extra.
|
|
|
8
8
|
|
|
9
9
|
You need a React project and `nanoeffects` installed. The form talks to your own client, `api` below: replace it with your requests.
|
|
10
10
|
|
|
11
|
+
The shortest version: a name field that asks the backend whether the name is free. The rest of the page builds a whole form from it.
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// username.js
|
|
15
|
+
import { createBus } from "nanoeffects";
|
|
16
|
+
import { latest } from "nanoeffects/policies";
|
|
17
|
+
|
|
18
|
+
export const bus = createBus(); // the bus, created once
|
|
19
|
+
|
|
20
|
+
// the name check, outside the component; latest aborts the check of the previous name
|
|
21
|
+
bus.on(
|
|
22
|
+
"username/typed",
|
|
23
|
+
latest(async (name, ctx) => {
|
|
24
|
+
const available = await api.usernameAvailable(name, { signal: ctx.signal });
|
|
25
|
+
ctx.emit("username/checked", available);
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```jsx
|
|
31
|
+
// NameField.jsx
|
|
32
|
+
import { useEffect, useState } from "react";
|
|
33
|
+
import { bus } from "./username";
|
|
34
|
+
|
|
35
|
+
export function NameField() {
|
|
36
|
+
const [available, setAvailable] = useState(null); // null: not checked yet
|
|
37
|
+
|
|
38
|
+
// on returns the unbind function; returning it removes the listener on unmount
|
|
39
|
+
useEffect(() => bus.on("username/checked", setAvailable), []);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
<input onChange={(event) => bus.emit("username/typed", event.target.value)} />
|
|
44
|
+
{available === false && <p>This name is taken</p>}
|
|
45
|
+
</>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The handler knows nothing about React, and the component knows nothing about requests.
|
|
51
|
+
|
|
11
52
|
## On this page
|
|
12
53
|
|
|
13
54
|
- [Problems of the form without a bus](#problems-of-the-form-without-a-bus)
|
package/docs/ru/README.md
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
nanoeffects выносит бизнес-логику и побочные эффекты из компонентов в обычные `async`-функции. Политики (`latest`, `debounce`, `queue`) и эффекты (`retry`, `timeout`, `poll`) решают гонки, повторы и отмену запросов.
|
|
4
4
|
|
|
5
5
|
```js
|
|
6
|
+
// search.js
|
|
6
7
|
import { createBus } from "nanoeffects";
|
|
7
8
|
import { latest } from "nanoeffects/policies";
|
|
8
9
|
|
|
9
|
-
const bus = createBus(); // шина событий
|
|
10
|
+
export const bus = createBus(); // шина событий
|
|
10
11
|
|
|
11
12
|
// Логика поиска живёт вне компонента; latest прерывает предыдущий запрос
|
|
12
13
|
bus.on(
|
|
@@ -22,7 +23,33 @@ bus.on("found", (items) => render(items));
|
|
|
22
23
|
bus.emit("search", "nano");
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
В компоненте та же шина не требует лишнего кода. Пример на React; обработчик остаётся в `search.js`:
|
|
27
|
+
|
|
28
|
+
```jsx
|
|
29
|
+
// SearchBox.jsx
|
|
30
|
+
import { useEffect, useState } from "react";
|
|
31
|
+
import { bus } from "./search";
|
|
32
|
+
|
|
33
|
+
export function SearchBox() {
|
|
34
|
+
const [items, setItems] = useState([]);
|
|
35
|
+
|
|
36
|
+
// on возвращает функцию отписки; если вернуть её из эффекта, подписка снимается при размонтировании
|
|
37
|
+
useEffect(() => bus.on("found", setItems), []);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
<input onChange={(event) => bus.emit("search", event.target.value)} />
|
|
42
|
+
<ul>
|
|
43
|
+
{items.map((item) => (
|
|
44
|
+
<li key={item.id}>{item.title}</li>
|
|
45
|
+
))}
|
|
46
|
+
</ul>
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Тот же пример по шагам: [React](react.md), [Vue](vue.md), [Svelte](svelte.md). Готовые проекты: [Проект из шаблона](starter.md).
|
|
26
53
|
|
|
27
54
|
## Установка
|
|
28
55
|
|
package/docs/ru/actions.md
CHANGED
|
@@ -141,15 +141,21 @@ export const sendMail = createAction("mail/send");
|
|
|
141
141
|
```js
|
|
142
142
|
// bus.js
|
|
143
143
|
import { createBus } from "nanoeffects";
|
|
144
|
-
import { actionsPlugin
|
|
145
|
-
import { sendMail } from "./mail";
|
|
144
|
+
import { actionsPlugin } from "nanoeffects/actions";
|
|
146
145
|
|
|
147
146
|
export const bus = createBus();
|
|
148
147
|
|
|
149
|
-
// actionsPlugin — реестр обработчиков этой
|
|
150
|
-
export const actions = bus.use(
|
|
151
|
-
|
|
152
|
-
|
|
148
|
+
// actionsPlugin — реестр обработчиков этой шины
|
|
149
|
+
export const actions = bus.use(actionsPlugin());
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
// mail-handlers.js
|
|
154
|
+
import { actions } from "./bus";
|
|
155
|
+
import { sendMail } from "./mail";
|
|
156
|
+
|
|
157
|
+
// actions.handle — привязывает работу к action на этой шине
|
|
158
|
+
actions.handle(sendMail, smtpSend);
|
|
153
159
|
```
|
|
154
160
|
|
|
155
161
|
```js
|
|
@@ -163,7 +169,7 @@ unbind();
|
|
|
163
169
|
|
|
164
170
|
Обработчик, привязанный в реестре, заставляет `bus.emit(sendMail(mail))` запустить его и вернуть его результат, как и для action со своим обработчиком. Подписки на этот тип получают сообщение первыми. Если обработчик не привязан, `sendMail` снова просто событие.
|
|
165
171
|
|
|
166
|
-
Реестр живёт в `nanoeffects/actions`, поэтому шина без него этого кода не содержит. Без реестра `emit` запускает собственный обработчик action. `bus.use(actionsPlugin())` задаёт `bus.actions`. Второй `actionsPlugin`
|
|
172
|
+
Реестр живёт в `nanoeffects/actions`, поэтому шина без него этого кода не содержит. Без реестра `emit` запускает собственный обработчик action. `bus.use(actionsPlugin())` задаёт `bus.actions`. Второй `actionsPlugin` возвращает тот же реестр. В `createBus` нет параметра `actions`. Собственный исполнитель назначьте через `bus.actions = { run }`, а добавив ему `bound(type)`, вы сможете отвечать за actions без собственного обработчика.
|
|
167
173
|
|
|
168
174
|
### Тип результата привязанного обработчика
|
|
169
175
|
|
|
@@ -47,7 +47,7 @@ bus.on("results", (items) => render(items));
|
|
|
47
47
|
|
|
48
48
|
Что заменить: `api.search` — ваша функция запроса. Она должна принимать `{ signal }` и передавать его в `fetch`. Без этого `latest` по-прежнему игнорирует старый результат, но запрос продолжает выполняться.
|
|
49
49
|
|
|
50
|
-
Чтобы ошибка стала невозможной, привяжите сигнал один раз: `
|
|
50
|
+
Чтобы ошибка стала невозможной, привяжите сигнал один раз: `signalFetch(ctx, url)` — это `fetch`, который останавливается вместе с обработчиком, а `effectsPlugin({ fetch: signalFetch })` даёт каждому обработчику `ctx.fetch` из него ([как](pass-the-signal.md#привязка-сигнала-один-раз)). Чтобы найти запросы, которые сигнал уже забывают, используйте `devPlugin` при разработке, а в тесте `getLostSignalsCount(bus)` ([как](pass-the-signal.md#подсчёт-потерянных-сигналов-в-тесте)).
|
|
51
51
|
|
|
52
52
|
Результат: после `emit("search", "ab")` и `emit("search", "abc")` вы увидите только результаты для `abc`, а запрос для `ab` отменится.
|
|
53
53
|
|
package/docs/ru/context.md
CHANGED
|
@@ -99,7 +99,7 @@ bus.on("user/open", async (id, ctx) => {
|
|
|
99
99
|
});
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
-
- `signalFetch` из той же точки входа
|
|
102
|
+
- Для `fetch` `fromSignal` не нужен: `signalFetch(ctx, url)` из той же точки входа останавливается вместе с задачей, а `effectsPlugin({ fetch: signalFetch })` даёт каждому обработчику `ctx.fetch`. [Привязка сигнала один раз](pass-the-signal.md#привязка-сигнала-один-раз)
|
|
103
103
|
- `make` запускается при первом чтении, поэтому обработчик, который никогда не использует `ctx.http`, ничего за него не платит.
|
|
104
104
|
- Задача, которую запускает `ctx.run`, строит своё значение из своего сигнала, так что прерывание дочерней задачи останавливает только её запросы.
|
|
105
105
|
- Поле не копируется через `{ ...ctx }`, и `Object.keys(ctx)` его не перечисляет. Если вы копируете контекст, сначала прочитайте `ctx.http`.
|
package/docs/ru/guide.md
CHANGED
|
@@ -376,7 +376,7 @@ bus.emit(double(21)); // 42
|
|
|
376
376
|
|
|
377
377
|
**Проблема.** Обработчик action лежит в файле фичи, а тесту нужна подделка, или работа принадлежит другому слою. Если положить обработчик в `createAction`, файл события окажется связан с работой.
|
|
378
378
|
|
|
379
|
-
**Что делать.** Создайте action без обработчика: это событие. Настоящий обработчик привяжите через `
|
|
379
|
+
**Что делать.** Создайте action без обработчика: это событие. Настоящий обработчик привяжите через `actions.handle` при создании шины. Следующий `actions.handle` привязывает новый обработчик, который главнее, пока вы его не снимете.
|
|
380
380
|
|
|
381
381
|
```js
|
|
382
382
|
// user.js
|
|
@@ -388,16 +388,16 @@ export const getUser = createAction("user/get"); // обработчика по
|
|
|
388
388
|
```js
|
|
389
389
|
// bus.js
|
|
390
390
|
import { createBus } from "nanoeffects";
|
|
391
|
-
import { actionsPlugin
|
|
391
|
+
import { actionsPlugin } from "nanoeffects/actions";
|
|
392
392
|
import { getUser } from "./user";
|
|
393
393
|
|
|
394
394
|
export const bus = createBus();
|
|
395
395
|
|
|
396
|
-
// setHandler — привязывает настоящий обработчик к action
|
|
397
|
-
const realUser = setHandler(getUser, (id) => api.user(id));
|
|
398
|
-
|
|
399
396
|
// actionsPlugin — реестр привязанных обработчиков этой шины
|
|
400
|
-
export const actions = bus.use(actionsPlugin(
|
|
397
|
+
export const actions = bus.use(actionsPlugin());
|
|
398
|
+
|
|
399
|
+
// actions.handle — привязывает настоящий обработчик к action
|
|
400
|
+
actions.handle(getUser, (id) => api.user(id));
|
|
401
401
|
```
|
|
402
402
|
|
|
403
403
|
```js
|
package/docs/ru/package.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
| `nanoeffects/actions` | `createAction`, `createJob`, `createModule`, `modulePlugin`, `actionsPlugin` | `emit` должен вернуть результат или доменная логика уходит из UI |
|
|
18
18
|
| `nanoeffects/match` | `matchPlugin`: `on`, `once` и `off` принимают функцию-фильтр вместо имени события | подписываться нужно по условию, а не по имени |
|
|
19
19
|
| `nanoeffects/context` | `contextPlugin`: ваши собственные поля (store, клиент) на `ctx` каждой задачи, с типами по значению | store или клиент должен быть на каждой задаче |
|
|
20
|
-
| `nanoeffects/signal` | `signalPlugin`: `emit(call, { signal })` привязывает задачи этого `emit` к внешнему `AbortSignal`; `
|
|
20
|
+
| `nanoeffects/signal` | `signalPlugin`: `emit(call, { signal })` привязывает задачи этого `emit` к внешнему `AbortSignal`; `signalFetch(ctx, url)`: `fetch`, который останавливается вместе с задачей; `fromSignal`: клиент, созданный из сигнала каждой задачи | внешний `AbortSignal` (запрос, React Query) должен останавливать задачи или обработчик не должен передавать сигнал вручную |
|
|
21
21
|
| `nanoeffects/oop` | `Emitter`, `extendBus`, `WithBus`, `On`, `Once`, `disposeBus`: методы шины на классе, подписки каждого экземпляра заканчиваются через `dispose()` | вы пишете классы, а не функции |
|
|
22
22
|
| `nanoeffects/scope` | `scopePlugin`, `effect`: поля для одного action, задачи, модуля или `emit` (типы `Scope`, `Effect`) | одному action или модулю нужны свои поля |
|
|
23
23
|
| `nanoeffects/redux` | `createEffectsMiddleware` | вы уже используете Redux |
|
|
@@ -22,25 +22,36 @@
|
|
|
22
22
|
|
|
23
23
|
## Привязка сигнала один раз
|
|
24
24
|
|
|
25
|
-
Надёжнее всего обработчик, в котором
|
|
25
|
+
Надёжнее всего обработчик, в котором сигнал не пишется вообще. `signalFetch` — это `fetch`, который принимает `ctx` первым аргументом, как `retry(ctx, ...)`: запрос несёт `ctx.signal`, и забыть нечего.
|
|
26
26
|
|
|
27
27
|
```js
|
|
28
|
-
//
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import axios from "axios";
|
|
28
|
+
// search-handlers.js
|
|
29
|
+
import { latest } from "nanoeffects/policies";
|
|
30
|
+
import { signalFetch } from "nanoeffects/signal";
|
|
31
|
+
import { bus } from "./bus";
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
fetch:
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
bus.on(
|
|
34
|
+
"search",
|
|
35
|
+
latest(async (query, ctx) => {
|
|
36
|
+
// signalFetch — fetch с ctx.signal: останавливается, когда политика отменяет обработчик
|
|
37
|
+
const response = await signalFetch(ctx, `/api?q=${query}`);
|
|
38
|
+
ctx.emit("results", await response.json());
|
|
40
39
|
}),
|
|
41
40
|
);
|
|
42
41
|
```
|
|
43
42
|
|
|
43
|
+
Чтобы `ctx.fetch` был в каждом обработчике, положите эффект на контекст один раз. `effectsPlugin` привязывает его к каждой задаче, поэтому обработчик обходится без аргумента `ctx`:
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
// bus.js
|
|
47
|
+
import { createBus } from "nanoeffects";
|
|
48
|
+
import { effectsPlugin } from "nanoeffects/effects";
|
|
49
|
+
import { signalFetch } from "nanoeffects/signal";
|
|
50
|
+
|
|
51
|
+
// effectsPlugin — ctx.fetch это signalFetch, привязанный к задаче, которая его читает
|
|
52
|
+
export const bus = createBus().use(effectsPlugin({ fetch: signalFetch }));
|
|
53
|
+
```
|
|
54
|
+
|
|
44
55
|
```js
|
|
45
56
|
// search-handlers.js
|
|
46
57
|
import { latest } from "nanoeffects/policies";
|
|
@@ -48,10 +59,27 @@ import { bus } from "./bus";
|
|
|
48
59
|
|
|
49
60
|
bus.on(
|
|
50
61
|
"search",
|
|
51
|
-
latest(async (query,
|
|
52
|
-
//
|
|
53
|
-
const response = await
|
|
54
|
-
|
|
62
|
+
latest(async (query, { fetch, emit }) => {
|
|
63
|
+
// ни сигнала, ни аргумента ctx: fetch останавливается, когда политика отменяет обработчик
|
|
64
|
+
const response = await fetch(`/api?q=${query}`);
|
|
65
|
+
emit("results", await response.json());
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Клиент, который не является эффектом, например экземпляр `axios`, создаётся из сигнала каждой задачи через `fromSignal`:
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
// bus.js
|
|
74
|
+
import { createBus } from "nanoeffects";
|
|
75
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
76
|
+
import { fromSignal } from "nanoeffects/signal";
|
|
77
|
+
import axios from "axios";
|
|
78
|
+
|
|
79
|
+
// fromSignal — ctx.http создаётся из сигнала задачи, которая его читает
|
|
80
|
+
export const bus = createBus().use(
|
|
81
|
+
contextPlugin({
|
|
82
|
+
http: fromSignal((signal) => axios.create({ signal })),
|
|
55
83
|
}),
|
|
56
84
|
);
|
|
57
85
|
```
|
|
@@ -105,7 +133,7 @@ bus.on(
|
|
|
105
133
|
| Ситуация | Что сделать | Линтер | devPlugin |
|
|
106
134
|
| -------- | ----------- | :----: | :-------: |
|
|
107
135
|
| У обработчика нет `ctx`: `latest(async (query) => ...)` | Возьмите `(query, ctx)` и передайте `ctx.signal` | да | |
|
|
108
|
-
| `ctx` взят, но в запрос не передан | Передайте `{ signal: ctx.signal }` в каждый запрос или привяжите его один раз: `ctx
|
|
136
|
+
| `ctx` взят, но в запрос не передан | Передайте `{ signal: ctx.signal }` в каждый запрос или привяжите его один раз: `signalFetch(ctx, url)` ([выше](#привязка-сигнала-один-раз)) | да | да |
|
|
109
137
|
| Один шаг передаёт сигнал, другой нет | Передайте его каждому шагу | да | да |
|
|
110
138
|
| Сигнал лежит в переменной: `const options = { signal: ctx.signal }` | Ничего: переменная с сигналом считается | принимает | |
|
|
111
139
|
| Вызовы идут вместе: `Promise.all([a(), b()])` | Передайте сигнал каждому вызову, а не одному | да | |
|
|
@@ -123,7 +151,7 @@ bus.on(
|
|
|
123
151
|
| Функция принимает сигнал и не использует его: `search(query, options) { return fetch(url); }` | Передайте сигнал дальше внутри неё. В линтере это находит только информация о типах; тест находит наверняка | типы | |
|
|
124
152
|
| Сигнал подключён вручную: `signal.addEventListener("abort", ...)` | Ничего: такой обработчик не проверяется по шагам | принимает | |
|
|
125
153
|
| `bus.emit` в обработчике | Используйте `ctx.emit`: только его отбрасывают после отмены обработчика | да | |
|
|
126
|
-
| Запрос продолжает идти после отмены, на Node | Передайте сигнал или используйте `ctx
|
|
154
|
+
| Запрос продолжает идти после отмены, на Node | Передайте сигнал или используйте `signalFetch(ctx, url)` | | да |
|
|
127
155
|
| Результаты отправлены после отмены | Обработчик проигнорировал сигнал. Передайте его или проверьте `ctx.signal.aborted` | | да |
|
|
128
156
|
| `ctx.on` после отмены | То же | | да |
|
|
129
157
|
|
package/docs/ru/policies.md
CHANGED
|
@@ -24,7 +24,7 @@ bus.on(
|
|
|
24
24
|
);
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Передайте `signal` в `fetch` (и подобные вызовы), чтобы запрос отменялся. Чтобы не передавать его вообще, привяжите сигнал один раз: с `ctx
|
|
27
|
+
Передайте `signal` в `fetch` (и подобные вызовы), чтобы запрос отменялся. Чтобы не передавать его вообще, привяжите сигнал один раз: с `signalFetch(ctx, url)` запрос останавливается вместе с обработчиком, и забыть нечего ([Сохранение сигнала в обработчике](pass-the-signal.md#привязка-сигнала-один-раз)).
|
|
28
28
|
|
|
29
29
|
## Таблица политик
|
|
30
30
|
|
package/docs/ru/react.md
CHANGED
|
@@ -8,6 +8,47 @@
|
|
|
8
8
|
|
|
9
9
|
Нужен проект на React с установленной `nanoeffects`. Форма общается с вашим клиентом, `api` ниже: замените его своими запросами.
|
|
10
10
|
|
|
11
|
+
Самый короткий вариант: поле имени, которое спрашивает backend, свободно ли оно. Остальная страница строит из него форму целиком.
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// username.js
|
|
15
|
+
import { createBus } from "nanoeffects";
|
|
16
|
+
import { latest } from "nanoeffects/policies";
|
|
17
|
+
|
|
18
|
+
export const bus = createBus(); // шина, создаётся один раз
|
|
19
|
+
|
|
20
|
+
// проверка имени вне компонента; latest прерывает проверку предыдущего имени
|
|
21
|
+
bus.on(
|
|
22
|
+
"username/typed",
|
|
23
|
+
latest(async (name, ctx) => {
|
|
24
|
+
const available = await api.usernameAvailable(name, { signal: ctx.signal });
|
|
25
|
+
ctx.emit("username/checked", available);
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```jsx
|
|
31
|
+
// NameField.jsx
|
|
32
|
+
import { useEffect, useState } from "react";
|
|
33
|
+
import { bus } from "./username";
|
|
34
|
+
|
|
35
|
+
export function NameField() {
|
|
36
|
+
const [available, setAvailable] = useState(null); // null: not checked yet
|
|
37
|
+
|
|
38
|
+
// on возвращает функцию отписки; если вернуть её из эффекта, подписка снимается при размонтировании
|
|
39
|
+
useEffect(() => bus.on("username/checked", setAvailable), []);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
<input onChange={(event) => bus.emit("username/typed", event.target.value)} />
|
|
44
|
+
{available === false && <p>This name is taken</p>}
|
|
45
|
+
</>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Обработчик ничего не знает о React, а компонент ничего не знает о запросах.
|
|
51
|
+
|
|
11
52
|
## На этой странице
|
|
12
53
|
|
|
13
54
|
- [Проблемы формы без шины](#проблемы-формы-без-шины)
|
package/docs/ru/svelte.md
CHANGED
|
@@ -6,6 +6,47 @@
|
|
|
6
6
|
|
|
7
7
|
Нужны Svelte 5 или новее и установленная `nanoeffects`. Не знакомы с шиной? Сначала прочитайте [Руководство](guide.md).
|
|
8
8
|
|
|
9
|
+
Самый короткий вариант: поле поиска, которое показывает найденное обработчиком. Плагин для этого не нужен; разделы ниже добавляют помощник, который сокращает компонент.
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
// search.js
|
|
13
|
+
import { createBus } from "nanoeffects";
|
|
14
|
+
import { latest } from "nanoeffects/policies";
|
|
15
|
+
|
|
16
|
+
export const bus = createBus(); // шина, создаётся один раз
|
|
17
|
+
|
|
18
|
+
// логика поиска вне компонента; latest прерывает предыдущий запрос
|
|
19
|
+
bus.on(
|
|
20
|
+
"search",
|
|
21
|
+
latest(async (query, ctx) => {
|
|
22
|
+
const items = await api.search(query, { signal: ctx.signal });
|
|
23
|
+
ctx.emit("found", items);
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Обработчик ничего не знает об UI, а компонент только отправляет `search` и слушает `found`.
|
|
29
|
+
|
|
30
|
+
<!-- docs-check: skip - a .svelte file -->
|
|
31
|
+
```svelte
|
|
32
|
+
<script>
|
|
33
|
+
import { bus } from "./search";
|
|
34
|
+
|
|
35
|
+
let items = $state([]);
|
|
36
|
+
|
|
37
|
+
// on возвращает функцию отписки; $effect вызывает её при очистке
|
|
38
|
+
$effect(() => bus.on("found", (found) => (items = found)));
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<input oninput={(event) => bus.emit("search", event.target.value)} />
|
|
42
|
+
<ul>
|
|
43
|
+
{#each items as item (item.id)}
|
|
44
|
+
<li>{item.title}</li>
|
|
45
|
+
{/each}
|
|
46
|
+
</ul>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
|
|
9
50
|
## Плагин на шине
|
|
10
51
|
|
|
11
52
|
**Проблема.** Компонент Svelte читает значение через `$store`, а событие шины — не store. Без помощи компонент пишет свой `readable` и `bus.on` и должен не забыть отписаться. Эти строки копируются в каждый компонент.
|
package/docs/ru/vue.md
CHANGED
|
@@ -6,6 +6,51 @@
|
|
|
6
6
|
|
|
7
7
|
Нужны Vue 3.3 или новее и установленная `nanoeffects`. Не знакомы с шиной? Сначала прочитайте [Руководство](guide.md).
|
|
8
8
|
|
|
9
|
+
Самый короткий вариант: поле поиска, которое показывает найденное обработчиком. Плагин для этого не нужен; разделы ниже добавляют помощник, который сокращает компонент.
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
// search.js
|
|
13
|
+
import { createBus } from "nanoeffects";
|
|
14
|
+
import { latest } from "nanoeffects/policies";
|
|
15
|
+
|
|
16
|
+
export const bus = createBus(); // шина, создаётся один раз
|
|
17
|
+
|
|
18
|
+
// логика поиска вне компонента; latest прерывает предыдущий запрос
|
|
19
|
+
bus.on(
|
|
20
|
+
"search",
|
|
21
|
+
latest(async (query, ctx) => {
|
|
22
|
+
const items = await api.search(query, { signal: ctx.signal });
|
|
23
|
+
ctx.emit("found", items);
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Обработчик ничего не знает об UI, а компонент только отправляет `search` и слушает `found`.
|
|
29
|
+
|
|
30
|
+
<!-- docs-check: skip - a .vue file -->
|
|
31
|
+
```vue
|
|
32
|
+
<script setup>
|
|
33
|
+
import { onUnmounted, ref } from "vue";
|
|
34
|
+
import { bus } from "./search";
|
|
35
|
+
|
|
36
|
+
const items = ref([]);
|
|
37
|
+
|
|
38
|
+
// on возвращает функцию отписки; при размонтировании вызываем её
|
|
39
|
+
const unbind = bus.on("found", (found) => {
|
|
40
|
+
items.value = found;
|
|
41
|
+
});
|
|
42
|
+
onUnmounted(unbind);
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<input @input="bus.emit('search', $event.target.value)" />
|
|
47
|
+
<ul>
|
|
48
|
+
<li v-for="item in items" :key="item.id">{{ item.title }}</li>
|
|
49
|
+
</ul>
|
|
50
|
+
</template>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
|
|
9
54
|
## Плагин на шине
|
|
10
55
|
|
|
11
56
|
**Проблема.** Без помощи компонент пишет `watch(query, (value) => bus.emit(queryChanged(value)))` и собственную подписку в `onMounted` и `onUnmounted`. Эти строки копируются в каждый компонент, и в каждой копии что-нибудь забыто: очистка, проверка равенства или имя подписки в трассировке.
|
package/docs/svelte.md
CHANGED
|
@@ -6,6 +6,47 @@ Use this page when your app is in Svelte 5 and a component has to show the resul
|
|
|
6
6
|
|
|
7
7
|
You need Svelte 5 or newer and `nanoeffects` installed. If you do not know what the bus is, read the [Guide](guide.md) first.
|
|
8
8
|
|
|
9
|
+
The shortest version: a search box that shows what the handler found. No plugin is needed for this; the sections below add a helper that shortens the component.
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
// search.js
|
|
13
|
+
import { createBus } from "nanoeffects";
|
|
14
|
+
import { latest } from "nanoeffects/policies";
|
|
15
|
+
|
|
16
|
+
export const bus = createBus(); // the bus, created once
|
|
17
|
+
|
|
18
|
+
// the search logic, outside the component; latest aborts the previous request
|
|
19
|
+
bus.on(
|
|
20
|
+
"search",
|
|
21
|
+
latest(async (query, ctx) => {
|
|
22
|
+
const items = await api.search(query, { signal: ctx.signal });
|
|
23
|
+
ctx.emit("found", items);
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The handler knows nothing about the UI, and the component only sends `search` and listens to `found`.
|
|
29
|
+
|
|
30
|
+
<!-- docs-check: skip - a .svelte file -->
|
|
31
|
+
```svelte
|
|
32
|
+
<script>
|
|
33
|
+
import { bus } from "./search";
|
|
34
|
+
|
|
35
|
+
let items = $state([]);
|
|
36
|
+
|
|
37
|
+
// on returns the unbind function; $effect calls it on cleanup
|
|
38
|
+
$effect(() => bus.on("found", (found) => (items = found)));
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<input oninput={(event) => bus.emit("search", event.target.value)} />
|
|
42
|
+
<ul>
|
|
43
|
+
{#each items as item (item.id)}
|
|
44
|
+
<li>{item.title}</li>
|
|
45
|
+
{/each}
|
|
46
|
+
</ul>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
|
|
9
50
|
## Add the plugin to the bus
|
|
10
51
|
|
|
11
52
|
**Problem.** A Svelte component reads a value with `$store`. An event of the bus is not a store, so without help the component writes its own `readable` and its own `bus.on`, and has to remember to stop listening. The same few lines are copied into every component.
|
package/docs/vue.md
CHANGED
|
@@ -6,6 +6,51 @@ Use this page when your app is in Vue 3 and a component has to talk to the bus:
|
|
|
6
6
|
|
|
7
7
|
You need Vue 3.3 or newer and `nanoeffects` installed. If you do not know what the bus is, read the [Guide](guide.md) first.
|
|
8
8
|
|
|
9
|
+
The shortest version: a search box that shows what the handler found. No plugin is needed for this; the sections below add a helper that shortens the component.
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
// search.js
|
|
13
|
+
import { createBus } from "nanoeffects";
|
|
14
|
+
import { latest } from "nanoeffects/policies";
|
|
15
|
+
|
|
16
|
+
export const bus = createBus(); // the bus, created once
|
|
17
|
+
|
|
18
|
+
// the search logic, outside the component; latest aborts the previous request
|
|
19
|
+
bus.on(
|
|
20
|
+
"search",
|
|
21
|
+
latest(async (query, ctx) => {
|
|
22
|
+
const items = await api.search(query, { signal: ctx.signal });
|
|
23
|
+
ctx.emit("found", items);
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The handler knows nothing about the UI, and the component only sends `search` and listens to `found`.
|
|
29
|
+
|
|
30
|
+
<!-- docs-check: skip - a .vue file -->
|
|
31
|
+
```vue
|
|
32
|
+
<script setup>
|
|
33
|
+
import { onUnmounted, ref } from "vue";
|
|
34
|
+
import { bus } from "./search";
|
|
35
|
+
|
|
36
|
+
const items = ref([]);
|
|
37
|
+
|
|
38
|
+
// on returns the unbind function; unmounting calls it
|
|
39
|
+
const unbind = bus.on("found", (found) => {
|
|
40
|
+
items.value = found;
|
|
41
|
+
});
|
|
42
|
+
onUnmounted(unbind);
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<input @input="bus.emit('search', $event.target.value)" />
|
|
47
|
+
<ul>
|
|
48
|
+
<li v-for="item in items" :key="item.id">{{ item.title }}</li>
|
|
49
|
+
</ul>
|
|
50
|
+
</template>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
|
|
9
54
|
## Add the plugin to the bus
|
|
10
55
|
|
|
11
56
|
**Problem.** Without help, a component writes `watch(query, (value) => bus.emit(queryChanged(value)))` and its own subscription in `onMounted` and `onUnmounted`. The same few lines are copied into every component, and each copy forgets something: the cleanup, the equality check, or the name of the subscription in the trace.
|