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.
- package/README.md +6 -5
- package/dist/{action-creator-CcAEvUnw.d.ts → action-creator-6rlBTmH9.d.ts} +60 -33
- package/dist/actions.d.ts +18 -18
- package/dist/actions.js +1 -1
- package/dist/bus-DFlmNJ6h.js +1 -0
- package/dist/context-B5Z2RpJt.js +1 -0
- package/dist/context-types-DKNc5mkS.d.ts +31 -0
- package/dist/context.d.ts +8 -3
- package/dist/context.js +1 -1
- package/dist/{delay-18LJ8jSk.js → delay-5OhJZZQ0.js} +1 -1
- package/dist/dev.d.ts +44 -3
- package/dist/dev.js +2 -1
- package/dist/effects.d.ts +2 -2
- package/dist/effects.js +1 -1
- package/dist/emit-key-C6FL7Ewn.js +1 -0
- package/dist/guard.d.ts +59 -0
- package/dist/guard.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/match.d.ts +3 -3
- package/dist/match.js +1 -1
- package/dist/{module-C_oEXw1n.d.ts → module-G39MaVOt.d.ts} +38 -38
- package/dist/notrace/{action-creator-DxOvVo8I.d.ts → action-creator-BrMSO3RP.d.ts} +35 -32
- package/dist/notrace/actions.d.ts +52 -52
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/{bus-DmKn4SDD.js → bus-37nlVljc.js} +1 -1
- package/dist/notrace/effects.d.ts +2 -2
- package/dist/notrace/index.d.ts +2 -2
- package/dist/notrace/index.js +1 -1
- package/dist/notrace/{plugin-lJjjd-SN.d.ts → plugin-7GUL2EW5.d.ts} +1 -1
- package/dist/notrace/policies.d.ts +1 -1
- package/dist/notrace/redux.d.ts +1 -1
- package/dist/observable.d.ts +43 -1
- package/dist/observable.js +1 -1
- package/dist/onAbort-BgMfspK4.js +1 -0
- package/dist/oop.d.ts +3 -3
- package/dist/oop.js +1 -1
- package/dist/{plugin-CuvIgjnX.d.ts → plugin-BWNP23hr.d.ts} +2 -2
- package/dist/policies.d.ts +2 -2
- package/dist/policies.js +1 -1
- package/dist/production/dev.js +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/redux.d.ts +1 -1
- package/dist/scope.d.ts +9 -9
- package/dist/signal-field-key-CWT-3OXC.js +1 -0
- package/dist/signal.d.ts +34 -6
- package/dist/signal.js +1 -1
- package/dist/site-DLvUoXs3.js +1 -0
- package/dist/svelte.d.ts +1 -1
- package/dist/trace.d.ts +170 -6
- package/dist/trace.js +3 -1
- package/dist/{types-FV8fuvI9.d.ts → types-N_cdRr1r.d.ts} +2 -2
- package/dist/vue.d.ts +1 -1
- package/dist/{with-policy-D-SQRkPh.js → with-policy-CeM1rtES.js} +1 -1
- package/dist/{with-policy-3bhFLEXs.d.ts → with-policy-CthiBp_z.d.ts} +1 -1
- package/docs/README.md +65 -22
- package/docs/actions.md +56 -13
- package/docs/cancel-stale-requests.md +2 -0
- package/docs/chat-example.md +15 -1
- package/docs/chat-widget.md +9 -7
- package/docs/context.md +352 -8
- package/docs/debounce-and-abort.md +3 -1
- package/docs/debugging-loops.md +283 -0
- package/docs/effects.md +259 -131
- package/docs/errors.md +4 -2
- package/docs/examples.md +60 -0
- package/docs/guide.md +57 -22
- package/docs/hmr.md +35 -1
- package/docs/loop-guard.md +109 -0
- package/docs/migrating-from-effector.md +15 -2
- package/docs/migrating-from-mitt.md +6 -4
- package/docs/migrating-from-rxjs.md +2 -0
- package/docs/migrating-from-saga.md +2 -0
- package/docs/motivation.md +14 -21
- package/docs/observable.md +45 -0
- package/docs/oop.md +20 -1
- package/docs/package.md +25 -13
- package/docs/pass-the-signal.md +281 -0
- package/docs/plugins.md +90 -15
- package/docs/policies.md +73 -46
- package/docs/prevent-double-submit.md +3 -1
- package/docs/query-libraries.md +276 -6
- package/docs/react.md +50 -11
- package/docs/recipes.md +528 -0
- package/docs/ru/README.md +62 -0
- package/docs/ru/actions.md +412 -0
- package/docs/ru/cancel-stale-requests.md +106 -0
- package/docs/ru/chat-example.md +259 -0
- package/docs/ru/chat-widget.md +131 -0
- package/docs/ru/context.md +401 -0
- package/docs/ru/debounce-and-abort.md +105 -0
- package/docs/ru/debugging-loops.md +283 -0
- package/docs/ru/effects.md +538 -0
- package/docs/ru/errors.md +36 -0
- package/docs/ru/examples.md +60 -0
- package/docs/ru/guide.md +530 -0
- package/docs/ru/hmr.md +83 -0
- package/docs/ru/loop-guard.md +109 -0
- package/docs/ru/migrating-from-effector.md +339 -0
- package/docs/ru/migrating-from-mitt.md +99 -0
- package/docs/ru/migrating-from-rxjs.md +197 -0
- package/docs/ru/migrating-from-saga.md +97 -0
- package/docs/ru/motivation.md +47 -0
- package/docs/ru/observable.md +233 -0
- package/docs/ru/oop.md +431 -0
- package/docs/ru/package.md +42 -0
- package/docs/ru/pass-the-signal.md +281 -0
- package/docs/ru/plugins.md +398 -0
- package/docs/ru/policies.md +201 -0
- package/docs/ru/prevent-double-submit.md +122 -0
- package/docs/ru/query-libraries.md +392 -0
- package/docs/ru/react.md +309 -0
- package/docs/ru/recipes.md +528 -0
- package/docs/ru/semantics.md +19 -0
- package/docs/ru/server.md +204 -0
- package/docs/ru/starter.md +118 -0
- package/docs/ru/stores.md +673 -0
- package/docs/ru/svelte.md +78 -0
- package/docs/ru/tracing.md +466 -0
- package/docs/ru/typescript.md +170 -0
- package/docs/ru/vue.md +92 -0
- package/docs/semantics.md +12 -0
- package/docs/server.md +5 -3
- package/docs/starter.md +2 -0
- package/docs/stores.md +2 -0
- package/docs/svelte.md +3 -1
- package/docs/tracing.md +132 -7
- package/docs/typescript.md +3 -1
- package/docs/vue.md +3 -1
- package/llms-full.txt +2718 -545
- package/llms.txt +17 -14
- package/package.json +25 -8
- package/skills/nanoeffects/SKILL.md +2 -2
- package/dist/bus-DlpEluzd.js +0 -1
- package/dist/context-UcmrAIhP.js +0 -1
- package/dist/context-types-hCKgU8sC.d.ts +0 -14
- package/docs/scope.md +0 -266
- package/docs/stability.md +0 -90
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Работа с TypeScript
|
|
2
|
+
|
|
3
|
+
> **[Уровень D](README.md#виды-проектов)**: доменная логика вне UI. Сначала прочитайте: [Руководство](guide.md#10-поддержка-typescript).
|
|
4
|
+
|
|
5
|
+
Обработчики и модули лежат в разных файлах. Как написать action с обработчиком, чтобы TypeScript вывел payload и результат, и что добавить, когда обработчикам нужны поля шины (`ctx.store`, эффекты из `effectsPlugin`). Для одного файла и для JavaScript страница не нужна.
|
|
6
|
+
|
|
7
|
+
## Action вместе с обработчиком
|
|
8
|
+
|
|
9
|
+
Payload берётся из аннотированного параметра, результат — из тела; обобщение писать не надо.
|
|
10
|
+
|
|
11
|
+
<!-- docs-check: skip - файл из examples/typescript, проверяется типами там -->
|
|
12
|
+
```ts
|
|
13
|
+
// user.ts
|
|
14
|
+
import { createAction, createModule } from "nanoeffects/actions";
|
|
15
|
+
|
|
16
|
+
export type User = { id: string; name: string };
|
|
17
|
+
|
|
18
|
+
// payload: string, из `id: string`; результат: Promise<User>, из тела
|
|
19
|
+
export const getUser = createAction(
|
|
20
|
+
"user/get",
|
|
21
|
+
async (id: string, { signal }) => {
|
|
22
|
+
const response = await fetch(`/api/users/${id}`, { signal });
|
|
23
|
+
const user: User = await response.json();
|
|
24
|
+
return user;
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// createModule: m.use привязывает обработчик, который несёт action, пока модуль подключён
|
|
29
|
+
export const userModule = createModule("user", (m) => {
|
|
30
|
+
m.use(getUser);
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Подключите модуль там, где создаёте шину. Результат типизируется по обработчику:
|
|
35
|
+
|
|
36
|
+
<!-- docs-check: skip - файл из examples/typescript, проверяется типами там -->
|
|
37
|
+
```ts
|
|
38
|
+
// main.ts
|
|
39
|
+
import { createBus } from "nanoeffects";
|
|
40
|
+
import { modulePlugin } from "nanoeffects/actions";
|
|
41
|
+
import { getUser, userModule } from "./user";
|
|
42
|
+
|
|
43
|
+
const bus = createBus();
|
|
44
|
+
const app = bus.use(modulePlugin([userModule])); // подключает модули
|
|
45
|
+
|
|
46
|
+
const user = bus.emit(getUser("1")); // Promise<User>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Что аннотировать и почему:
|
|
50
|
+
|
|
51
|
+
| Часть | Аннотировать? | Почему |
|
|
52
|
+
| ----- | ------------- | ------ |
|
|
53
|
+
| payload (`id: string`) | да | у параметра функции нет типа, из которого можно вывести |
|
|
54
|
+
| `ctx` | нет, для `signal` и полей, которые есть у каждой задачи | `signal`, `emit`, `on` и `run` есть на каждом `ctx` |
|
|
55
|
+
| результат | нет | выводится из тела |
|
|
56
|
+
|
|
57
|
+
Неверная payload при вызове (`getUser(1)`) — ошибка. Аннотируйте параметры, а не переменную: тип на всей переменной, например `const handler: Task<string, Events> = …`, фиксирует тип функции, и результат становится `unknown`.
|
|
58
|
+
|
|
59
|
+
## Поля шины в `ctx`
|
|
60
|
+
|
|
61
|
+
Типы шины живут на шине. `contextPlugin({ store })` и `effectsPlugin({ race })` делают `ctx.store` и `ctx.race` известными обработчикам **этой шины**. Action с обработчиком, созданное в другом файле, шины не видит, поэтому на его `ctx` нет `store`:
|
|
62
|
+
|
|
63
|
+
<!-- docs-check: skip - намеренно показывает ошибку -->
|
|
64
|
+
```ts
|
|
65
|
+
export const getUser = createAction("user/get", (id: string, ctx) => {
|
|
66
|
+
ctx.store; // ошибка: Property 'store' does not exist
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Во время выполнения шина всё равно даёт задаче её поля, не знает об этом только проверка типов. Назовите типы шины один раз в файле, который её создаёт. `contextPlugin` берёт тип каждого поля из переданного значения, поэтому карта событий описывает только события. `EventsOf` читает из шины карту событий вместе с полями, добавленными плагинами. Код этого раздела лежит в [`examples/typescript-context`](../../examples/typescript-context).
|
|
71
|
+
|
|
72
|
+
<!-- docs-check: skip - файл из examples/typescript-context, проверяется типами там -->
|
|
73
|
+
```ts
|
|
74
|
+
// bus.ts
|
|
75
|
+
import { type Context, createBus, type EventsOf } from "nanoeffects";
|
|
76
|
+
import { modulePlugin } from "nanoeffects/actions";
|
|
77
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
78
|
+
import { effectsPlugin, race } from "nanoeffects/effects";
|
|
79
|
+
|
|
80
|
+
// Events: имя события → payload; о ctx здесь ничего не пишется
|
|
81
|
+
type Events = { "user/loaded": User };
|
|
82
|
+
|
|
83
|
+
export const bus = createBus<Events>()
|
|
84
|
+
.use(contextPlugin({ store })) // ctx.store, тип выводится из значения `store`
|
|
85
|
+
.use(effectsPlugin({ race })); // ctx.race; каждый плагин возвращает ту же шину, типизированную своими полями
|
|
86
|
+
|
|
87
|
+
// приложение шины: подключает модули и создаёт их с типами шины
|
|
88
|
+
export const app = bus.use(modulePlugin()); // модулей пока нет: их подключает main.ts
|
|
89
|
+
export const { createModule } = app; // createModule с типами шины
|
|
90
|
+
|
|
91
|
+
export type AppEvents = EventsOf<typeof bus>; // карта событий и все поля ctx
|
|
92
|
+
export type AppContext = Context<AppEvents>; // ctx обработчика этой шины: есть store и race
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
| Тип | Что это | Зачем его называть |
|
|
96
|
+
| --- | ------- | ------------------ |
|
|
97
|
+
| `EventsOf<typeof bus>` | карта событий шины с полями контекста плагинов (и `WithContext`, если вы его используете) | источник для `AppContext` |
|
|
98
|
+
| `Context<AppEvents>` | `ctx`, который получает обработчик шины | аннотируйте `ctx` обработчика, переданного в `createAction` |
|
|
99
|
+
|
|
100
|
+
Эти типы ничего не меняют во время выполнения. Они лишь сообщают проверке типов то, что шина уже делает.
|
|
101
|
+
|
|
102
|
+
Аннотируйте `ctx` через `AppContext` там, где обработчик читает поле шины, и создавайте модуль через `createModule`, который экспортирует `bus.ts`, чтобы обработчики его `m.on` тоже видели поля:
|
|
103
|
+
|
|
104
|
+
<!-- docs-check: skip - файл из examples/typescript-context, проверяется типами там -->
|
|
105
|
+
```ts
|
|
106
|
+
// user.ts
|
|
107
|
+
import { createAction } from "nanoeffects/actions";
|
|
108
|
+
import { type AppContext, createModule } from "./bus";
|
|
109
|
+
|
|
110
|
+
export const getUser = createAction(
|
|
111
|
+
"user/get",
|
|
112
|
+
(id: string, { store, signal }: AppContext) => store.fetchUser(id, { signal }),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// createModule из bus.ts: обработчики модуля видят поля контекста шины
|
|
116
|
+
export const userModule = createModule("user", (m) => {
|
|
117
|
+
m.use(getUser);
|
|
118
|
+
m.on("user/loaded", (user, { store }) => store.remember(user));
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
<!-- docs-check: skip - файл из examples/typescript-context, проверяется типами там -->
|
|
123
|
+
```ts
|
|
124
|
+
// main.ts: модули подключаются здесь, потому что файлам модулей нужен `app`
|
|
125
|
+
import { app, bus } from "./bus";
|
|
126
|
+
import { getUser, userModule } from "./user";
|
|
127
|
+
|
|
128
|
+
const unmount = app.mount(userModule); // запускает модуль; unmount() его отменяет
|
|
129
|
+
const user = bus.emit(getUser("1")); // Promise<User>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`createModule` приложения только создаёт модуль: он его не подключает и не хранит связи с шиной. Поэтому приложение в `bus.ts` стартует без модулей. `createModule` из `nanoeffects/actions` остаётся для модуля, созданного без шины: там карту событий вы пишете сами, `createModule<AppEvents>("user", …)`.
|
|
133
|
+
|
|
134
|
+
## Типы функции, принимающей сигнал
|
|
135
|
+
|
|
136
|
+
Функция, которая делает запрос, должна принимать `signal` задачи, тогда прерывание задачи отменит запрос. `WithSignal` — тип такого параметра: объект с `signal`. Как `WithContext`, он нужен только для проверки типов и в бандл ничего не добавляет.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import type { WithSignal } from "nanoeffects";
|
|
140
|
+
|
|
141
|
+
// WithSignal — { signal: AbortSignal }; ему подходит контекст задачи и просто { signal }
|
|
142
|
+
const search = (query: string, { signal }: WithSignal) =>
|
|
143
|
+
fetch(`/search?q=${encodeURIComponent(query)}`, { signal });
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Передавайте `ctx` (или `{ signal }`) при вызове. Эффекты из `nanoeffects/effects` (`retry`, `timeout`, `all`) вместо этого принимают необязательный сигнал, потому что вне задачи можно передать `{}`; это другой тип, и писать его не нужно.
|
|
147
|
+
|
|
148
|
+
## Контракт отдельно от кода
|
|
149
|
+
|
|
150
|
+
Если файл с action не должен импортировать код, который его реализует, создайте action без обработчика. Payload и результат вы пишете один раз как `Call`, а обработчик уходит в модуль, где аннотации ему не нужны. `Call` — ваше обещание: ничто не сверяет его с обработчиком, поэтому держите обработчик в модуле типизированным по нему:
|
|
151
|
+
|
|
152
|
+
<!-- docs-check: skip - фрагмент файла выше -->
|
|
153
|
+
```ts
|
|
154
|
+
export const getUser = createAction<Call<string, Promise<User>>>("user/get"); // контракт
|
|
155
|
+
|
|
156
|
+
createModule("user", (m) => {
|
|
157
|
+
// payload и результат берутся из Call, ctx — из приложения: аннотации нет
|
|
158
|
+
m.handle(getUser, (id, { store, signal }) => store.fetchUser(id, { signal }));
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Неверная payload, неверное поле `ctx` и неверный результат — ошибки.
|
|
163
|
+
|
|
164
|
+
## Частые ошибки
|
|
165
|
+
|
|
166
|
+
- **Поля нет на `ctx`.** `ctx` обработчика, переданного в `createAction`, не аннотирован: добавьте `: AppContext`. Обработчику, написанному прямо в `m.handle` или `m.on` модуля, созданного через `createModule` из `bus.ts`, это не нужно. У `ctx` по умолчанию полей вашей шины нет.
|
|
167
|
+
- **`EventsOf<typeof bus>` равен `never`.** `bus` — не шина. `bus.use(actionsPlugin())` и `bus.use(modulePlugin(...))` возвращают реестр или приложение, а не шину: держите шину в её собственной константе (`bus`, затем `app = bus.use(modulePlugin())`).
|
|
168
|
+
- **`effectsPlugin` нет в типе.** `use` должен идти раньше, чем вы читаете `typeof bus`. Стройте цепочку `createBus(...).use(effectsPlugin(...))` в одной привязке.
|
|
169
|
+
- **Политика теряет типы?** Нет: `latest(handler)` сохраняет payload, результат и контекст `handler`.
|
|
170
|
+
- **Не передавайте контекст обобщением `createAction`.** Это четвёртый параметр обобщения формы с обработчиком, после payload, результата и строки типа, так что пришлось бы писать все четыре. Аннотируйте `ctx` через `AppContext` или положите обработчик в модуль, созданный через `createModule` из `bus.ts`.
|
package/docs/ru/vue.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# nanoeffects в приложении на Vue
|
|
2
|
+
|
|
3
|
+
> **[Уровень B](README.md#виды-проектов)**: SPA с async UI. Сначала прочитайте: [Руководство](guide.md#1-создание-шины-событий).
|
|
4
|
+
|
|
5
|
+
Задача: компонент на Vue 3 должен сообщить шине, что значение изменилось (например, текст в поле поиска), и показать результат события (найденные элементы). Для этого `vuePlugin` даёт две функции. Работающий пример — шаблон `vue-ts` из [Проекта из шаблона](starter.md#начало-с-vue).
|
|
6
|
+
|
|
7
|
+
Нужны Vue 3.3 или новее и установленная `nanoeffects`. Не знакомы с шиной? Сначала прочитайте [Руководство](guide.md).
|
|
8
|
+
|
|
9
|
+
## Плагин на шине
|
|
10
|
+
|
|
11
|
+
**Проблема.** Без помощи компонент пишет `watch(query, (value) => bus.emit(queryChanged(value)))` и собственную подписку в `onMounted` и `onUnmounted`. Эти строки копируются в каждый компонент, и в каждой копии что-нибудь забыто: очистка, проверка равенства или имя подписки в трассировке.
|
|
12
|
+
|
|
13
|
+
**Когда.** Компонент Vue отправляет значения в шину, читает из неё события или делает и то и другое.
|
|
14
|
+
|
|
15
|
+
`vuePlugin()` добавляет на шину две функции. Достаньте их из шины один раз в файле, который её создаёт, и импортируйте где нужно:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
// bus.js
|
|
19
|
+
import { createBus } from "nanoeffects";
|
|
20
|
+
import { vuePlugin } from "nanoeffects/vue";
|
|
21
|
+
|
|
22
|
+
// createBus — шина событий, создаётся один раз вне Vue
|
|
23
|
+
// vuePlugin — добавляет на шину watchEmitter и useEventRef
|
|
24
|
+
export const bus = createBus().use(vuePlugin());
|
|
25
|
+
export const { watchEmitter, useEventRef } = bus; // свободные функции: они не читают `this`
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Сообщение шине об изменении значения
|
|
29
|
+
|
|
30
|
+
<!-- docs-check: continue -->
|
|
31
|
+
|
|
32
|
+
`watchEmitter(source, event)` следит за `ref` или геттером и отправляет `event` с новым значением. При вызове он ничего не отправляет, а при размонтировании компонента останавливается. Это `watch`, который заканчивается `emit`, поэтому действуют правила `watch`: несколько изменений за один тик дают одно событие с последним значением, а изменение объекта в `ref` на месте его не запускает.
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
// search.js
|
|
36
|
+
import { createAction } from "nanoeffects/actions";
|
|
37
|
+
|
|
38
|
+
// createAction — событие с именем; вызов строит { type, payload }
|
|
39
|
+
export const queryChanged = createAction("search/queryChanged");
|
|
40
|
+
export const searchLoaded = createAction("search/loaded");
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
// query-box.js
|
|
45
|
+
import { ref } from "vue";
|
|
46
|
+
import { watchEmitter } from "./bus";
|
|
47
|
+
import { queryChanged } from "./search";
|
|
48
|
+
|
|
49
|
+
export const setup = () => {
|
|
50
|
+
const query = ref("");
|
|
51
|
+
|
|
52
|
+
// watchEmitter — отправляет queryChanged(query), когда query меняется, но не при монтировании
|
|
53
|
+
watchEmitter(query, queryChanged);
|
|
54
|
+
|
|
55
|
+
return { query };
|
|
56
|
+
};
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Третий аргумент принимает параметры `equals`, `traceId` и `traceGroupId`:
|
|
60
|
+
|
|
61
|
+
| Параметр | Когда использовать |
|
|
62
|
+
| -------- | ------------------ |
|
|
63
|
+
| `equals(previous, next)` | Новый объект с теми же полями не должен отправляться: `{ equals: (a, b) => a.id === b.id }`. |
|
|
64
|
+
| `traceId` | Нужно имя подписки в [трассировке](tracing.md#какая-подписка-начала-цепочку). |
|
|
65
|
+
| `traceGroupId` | Нужно назвать весь поток, который запускает эта отправка. |
|
|
66
|
+
|
|
67
|
+
`watchEmitter` возвращает функцию, которая останавливает его досрочно. Она нужна вне компонента.
|
|
68
|
+
|
|
69
|
+
## Показ результата события
|
|
70
|
+
|
|
71
|
+
<!-- docs-check: continue -->
|
|
72
|
+
|
|
73
|
+
`useEventRef(event, initial)` возвращает `ref`. Сначала в нём `initial`, затем payload каждого `event` шины. При размонтировании компонента `ref` перестаёт слушать. Если payload — функция, `ref` сохраняет её, а не вызывает.
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
// found-list.js
|
|
77
|
+
import { useEventRef } from "./bus";
|
|
78
|
+
import { searchLoaded } from "./search";
|
|
79
|
+
|
|
80
|
+
export const setup = () => {
|
|
81
|
+
// useEventRef — как ref([]), но значение устанавливает searchLoaded
|
|
82
|
+
const found = useEventRef(searchLoaded, []);
|
|
83
|
+
|
|
84
|
+
return { found };
|
|
85
|
+
};
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
В TypeScript тип `ref` берётся из события: для `createAction<string[]>("search/loaded")` это `Ref<string[]>`, а для события без типа payload он берётся из `initial`.
|
|
89
|
+
|
|
90
|
+
### Это не store
|
|
91
|
+
|
|
92
|
+
`useEventRef` — state одного компонента. Каждый вызывающий его компонент хранит свою копию последней payload. Шина значений не хранит, поэтому событие, пришедшее до вызова, теряется. Чтобы делить значение между компонентами или сохранить его для будущих, держите его в store, например в Pinia, и записывайте туда из обработчика события. Либо вызовите `useEventRef` один раз в корневом компоненте и передайте `ref` вниз через `provide` и `inject`.
|
package/docs/semantics.md
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
# Semantics
|
|
2
2
|
|
|
3
|
+
> **Level F.** Read first: [Guide](guide.md#1-create-a-bus) steps 1 to 6.
|
|
4
|
+
|
|
3
5
|
Guarantees and sharp edges. Read when something surprises you; not required to start.
|
|
4
6
|
|
|
7
|
+
## Five rules to know first
|
|
8
|
+
|
|
9
|
+
1. A handler is `(payload, ctx)`. Use `ctx.signal`, `ctx.emit` and `ctx.on`; the rest of `ctx` is optional.
|
|
10
|
+
2. Pass `ctx.signal` to what the handler calls (`fetch`, a client), so an abort stops the work.
|
|
11
|
+
3. An aborted task delivers no result and emits nothing.
|
|
12
|
+
4. `emit` is synchronous: the first handler error is thrown, unless `onError` is set.
|
|
13
|
+
5. Rejections of async handlers are not handled without `onError`.
|
|
14
|
+
|
|
15
|
+
Details of each rule and the rest:
|
|
16
|
+
|
|
5
17
|
- **An aborted task delivers no result.** If the task aborts before a promise settles, the promise rejects with the abort reason, even if the value was just produced in the same tick. `latest`, `debounce`, `all`, `race`, `take`, and the rest follow this rule.
|
|
6
18
|
- **`emit` is a plain, synchronous loop** (see [Errors](errors.md)). A `ctx.emit` inside a handler is the same call: without `onError`, an error of a nested handler is thrown into the handler that emitted; with `onError` it goes there and `ctx.emit` does not throw.
|
|
7
19
|
- **Rejections of async handlers** are not handled for you. Without `onError` they are unhandled rejections, which end a Node process by default. Use `onError` for async handlers.
|
package/docs/server.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Stop work on a server when the client leaves
|
|
2
2
|
|
|
3
|
+
> **Level C.** Read first: [Guide](guide.md#1-create-a-bus) steps 1 to 3.
|
|
4
|
+
|
|
3
5
|
Read this page if you run a Node server (the examples use [Fastify](https://fastify.dev)) and want the work for a request, a socket or a queued job to stop when nobody needs it. It shows one pattern in three places: a request, a WebSocket and a worker.
|
|
4
6
|
|
|
5
7
|
You need Node 18 or newer, `fastify` and `nanoeffects`. `api` below is your own client for another service: replace it with your requests.
|
|
@@ -37,7 +39,7 @@ The action loads a user. It tries the request up to three times, and gives up on
|
|
|
37
39
|
import { createAction } from "nanoeffects/actions";
|
|
38
40
|
import { retry, timeout } from "nanoeffects/effects";
|
|
39
41
|
|
|
40
|
-
// createAction —
|
|
42
|
+
// createAction — an action with a handler: emit returns what the handler returns
|
|
41
43
|
export const getUser = createAction("user/get", async (id, ctx) => {
|
|
42
44
|
// one try: the request, with the signal of timeout so it can stop it
|
|
43
45
|
const load = ({ signal }) => api.getUser(id, { signal });
|
|
@@ -78,7 +80,7 @@ app.get("/users/:id", async (request, reply) => {
|
|
|
78
80
|
});
|
|
79
81
|
|
|
80
82
|
try {
|
|
81
|
-
// emit with { signal } — aborting the controller aborts the
|
|
83
|
+
// emit with { signal } — aborting the controller aborts the action: timeout, retry and the request
|
|
82
84
|
const call = getUser(request.params.id);
|
|
83
85
|
return await bus.emit(call, { signal: controller.signal });
|
|
84
86
|
} catch (error) {
|
|
@@ -131,7 +133,7 @@ import { createAction } from "nanoeffects/actions";
|
|
|
131
133
|
import { poll } from "nanoeffects/effects";
|
|
132
134
|
import { latest } from "nanoeffects/policies";
|
|
133
135
|
|
|
134
|
-
// createAction —
|
|
136
|
+
// createAction — an action with a handler
|
|
135
137
|
// latest — a new subscription stops the old one; key — but only the old one of the same socket
|
|
136
138
|
export const watchPrices = createAction(
|
|
137
139
|
"prices/watch",
|
package/docs/starter.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Start a project from a template
|
|
2
2
|
|
|
3
|
+
> **Level A.** Read first: nothing: this page makes a project for you.
|
|
4
|
+
|
|
3
5
|
Use this page when you want a running project in one command instead of wiring Vite and the bus by hand. It makes a small app with a search box that waits for you to stop typing (`debounce`) and aborts the older request when a newer one starts (`latest`).
|
|
4
6
|
|
|
5
7
|
You need Node 18 or newer.
|
package/docs/stores.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Connect Redux, Zustand, MobX, or Pinia
|
|
2
2
|
|
|
3
|
+
> **Level E.** Read first: [Guide](guide.md#1-create-a-bus) steps 1 to 3.
|
|
4
|
+
|
|
3
5
|
Use this page when you **already have a store** and want async work with cancellation and policies next to it. It is not a reason to pick Redux (or any store).
|
|
4
6
|
|
|
5
7
|
If you do not have a store yet, use `createBus` from the [Guide](guide.md).
|
package/docs/svelte.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Use nanoeffects in a Svelte app
|
|
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 your app is in Svelte 5 and a component has to show the result of an event. `sveltePlugin` gives you one function for that, `eventStore`. To see it in a running project, start from the `svelte-ts` template in [Start a project from a template](starter.md#start-with-svelte).
|
|
4
6
|
|
|
5
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.
|
|
@@ -78,4 +80,4 @@ bus.on(searchLoaded, (titles) => foundTitles.set(titles));
|
|
|
78
80
|
## Where to go next
|
|
79
81
|
|
|
80
82
|
- The same job for React: [Build a sign-up form in React](react.md); for Vue: [Use nanoeffects in a Vue app](vue.md).
|
|
81
|
-
- A policy in a component (a debounced save): in the script of a component make it once with [`withPolicy`](policies.md#
|
|
83
|
+
- A policy in a component (a debounced save): in the script of a component make it once with [`withPolicy`](policies.md#withpolicy-call-a-policy-outside-a-bus); Svelte runs the script once, so it needs no hook.
|
package/docs/tracing.md
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
# Trace tasks
|
|
2
2
|
|
|
3
|
+
> **Level F.** Read first: [Guide](guide.md#1-create-a-bus) steps 1 and 2.
|
|
4
|
+
|
|
3
5
|
Find out what your app did and why: which events were emitted, which calls a policy cancelled or dropped, which request failed. You see it in the browser console, in Redux DevTools, or in the Performance panel.
|
|
4
6
|
|
|
5
7
|
Tracing is optional. A bus works the same without it, and it costs almost nothing until you turn it on. If you only want to see what happens while you develop, jump to [Show every emit](#show-every-emit) and [Configure Redux DevTools](#configure-redux-devtools); the rest of the page is for apps that need to follow a flow or turn tracing off in a build.
|
|
6
8
|
|
|
9
|
+
## On this page
|
|
10
|
+
|
|
11
|
+
- [Attach a tracer](#attach-a-tracer)
|
|
12
|
+
- [Show every emit](#show-every-emit)
|
|
13
|
+
- [Show which subscription began a chain](#show-which-subscription-began-a-chain)
|
|
14
|
+
- [Follow one flow with a traceGroupId](#follow-one-flow-with-a-tracegroupid)
|
|
15
|
+
- [Name a listener](#name-a-listener)
|
|
16
|
+
- [Keep the chain: `ctx.emit`, not `bus.emit`](#keep-the-chain-ctxemit-not-busemit)
|
|
17
|
+
- [Turn the names off](#turn-the-names-off)
|
|
18
|
+
- [Use built-in loggers](#use-built-in-loggers)
|
|
19
|
+
- [Cut volume in production](#cut-volume-in-production)
|
|
20
|
+
- [Keep the last events for an incident](#keep-the-last-events-for-an-incident)
|
|
21
|
+
- [Configure Redux DevTools](#configure-redux-devtools)
|
|
22
|
+
- [Find loops with `graphLogger`](#find-loops-with-graphlogger)
|
|
23
|
+
- [Send safe data to a tool](#send-safe-data-to-a-tool)
|
|
24
|
+
- [Write your own logger](#write-your-own-logger)
|
|
25
|
+
- [Drop tracing from a build](#drop-tracing-from-a-build)
|
|
26
|
+
|
|
7
27
|
## Attach a tracer
|
|
8
28
|
|
|
9
29
|
Pass `trace` to `createBus`. Make it with `createTracer` from `nanoeffects/trace`, which takes a logger (a function) or an array of them. The tracing code lives in that entry, so a bus without `trace` does not carry it.
|
|
@@ -39,7 +59,7 @@ Phases:
|
|
|
39
59
|
- `cancel` — aborted after it started (superseded by `latest`, parent aborted, lost a `race`).
|
|
40
60
|
- `drop` — never started (`leading` while busy, a `throttle` call replaced by a newer one, a `debounce` call replaced before its delay ended).
|
|
41
61
|
|
|
42
|
-
`all` and `race` are traced when you pass them the handler `ctx`: the group is one task; every
|
|
62
|
+
`all` and `race` are traced when you pass them the handler `ctx`: the group is one task; every function of the group is a child `run` task.
|
|
43
63
|
|
|
44
64
|
```js
|
|
45
65
|
import { createBus } from "nanoeffects";
|
|
@@ -86,7 +106,7 @@ The event changes nothing else: the other tasks keep their `parentId`, `rootId`
|
|
|
86
106
|
|
|
87
107
|
## Show which subscription began a chain
|
|
88
108
|
|
|
89
|
-
**Problem.** `useEmitter` and `bus.emitter` put an event on the bus from outside any task, such as a render or the callback of a store. In the trace that event has no sign of where it came from. With many such subscriptions it is hard to tell which one started a chain of
|
|
109
|
+
**Problem.** `useEmitter` and `bus.emitter` put an event on the bus from outside any task, such as a render or the callback of a store. In the trace that event has no sign of where it came from. With many such subscriptions it is hard to tell which one started a chain of actions and retries.
|
|
90
110
|
|
|
91
111
|
**When.** The bus has a tracer and the project has several `useEmitter` or `bus.emitter` calls. The names come from `tracePlugin`: use it on the bus, or the `traceId` is ignored.
|
|
92
112
|
|
|
@@ -114,8 +134,8 @@ In a component, the same option goes in the options of the hook: `useEmitter(nam
|
|
|
114
134
|
|
|
115
135
|
```
|
|
116
136
|
[nanoeffects] Profile · run start
|
|
117
|
-
[nanoeffects] user/save ·
|
|
118
|
-
[nanoeffects] user/save ·
|
|
137
|
+
[nanoeffects] user/save · action start
|
|
138
|
+
[nanoeffects] user/save · action done
|
|
119
139
|
[nanoeffects] Profile · run done
|
|
120
140
|
```
|
|
121
141
|
|
|
@@ -155,7 +175,7 @@ const pay = createAction("checkout/pay", async () => {}, {
|
|
|
155
175
|
```
|
|
156
176
|
|
|
157
177
|
```
|
|
158
|
-
[nanoeffects] [checkout] checkout/pay ·
|
|
178
|
+
[nanoeffects] [checkout] checkout/pay · action start
|
|
159
179
|
```
|
|
160
180
|
|
|
161
181
|
The emitters take it too: `useEmitter(cartChanged, cart, { traceId: "Cart", traceGroupId: "checkout" })`. `traceGroupId` works without `scopePlugin`, and needs `tracePlugin`.
|
|
@@ -185,7 +205,7 @@ bus.once("name/changed", (name) => console.log(name), { traceId: "auditLog" });
|
|
|
185
205
|
|
|
186
206
|
```
|
|
187
207
|
[nanoeffects] saveNotifier · run start
|
|
188
|
-
[nanoeffects] user/save ·
|
|
208
|
+
[nanoeffects] user/save · action start
|
|
189
209
|
[nanoeffects] saveNotifier · run done
|
|
190
210
|
```
|
|
191
211
|
|
|
@@ -230,7 +250,8 @@ const bus = createBus({
|
|
|
230
250
|
| `reduxDevtools()` | Sends the emits to Redux DevTools, and with `policies` the stages of the handlers as `type/effect/phase`; state is `{ running }` |
|
|
231
251
|
| `consoleLogger()` | Prints one line per phase, indented by task depth |
|
|
232
252
|
| `performanceLogger()` | User Timing measures for the browser Performance panel |
|
|
233
|
-
| `createTraceRecorder()` | Keeps events in `recorder.events
|
|
253
|
+
| `createTraceRecorder()` | Keeps events in `recorder.events`: all of them for tests, or with `{ capacity }` the last N ([see below](#keep-the-last-events-for-an-incident)) |
|
|
254
|
+
| `graphLogger()` | Builds the map of which event leads to which, finds loops while the app runs, and writes the map as Mermaid, Graphviz or JSON ([see below](#find-loops-with-graphlogger)) |
|
|
234
255
|
|
|
235
256
|
### Cut volume in production
|
|
236
257
|
|
|
@@ -246,8 +267,60 @@ createTracer(logger, {
|
|
|
246
267
|
- `sample` is decided once per root task; every task below it follows, so a trace is never cut in the middle. Unsampled tasks cost almost nothing.
|
|
247
268
|
- `filter` runs on each event before any logger sees it.
|
|
248
269
|
|
|
270
|
+
### Keep the last events for an incident
|
|
271
|
+
|
|
272
|
+
`createTraceRecorder()` with no option keeps every event, so a long session fills memory. That is fine in a test and wrong in a running app. Give it a `capacity` and it keeps only the last N events (a ring buffer): when something goes wrong, you still have what led up to it.
|
|
273
|
+
|
|
274
|
+
<!-- docs-check: new -->
|
|
275
|
+
|
|
276
|
+
```js
|
|
277
|
+
// bus.js
|
|
278
|
+
import { createBus } from "nanoeffects";
|
|
279
|
+
import { createTraceRecorder, createTracer } from "nanoeffects/trace";
|
|
280
|
+
|
|
281
|
+
// A logger that keeps the last 500 events and drops the older ones.
|
|
282
|
+
export const recorder = createTraceRecorder({ capacity: 500 });
|
|
283
|
+
|
|
284
|
+
// Feed the recorder from the tracer.
|
|
285
|
+
const trace = createTracer(recorder);
|
|
286
|
+
|
|
287
|
+
export const bus = createBus({ trace });
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
// incident.js
|
|
292
|
+
import { recorder } from "./bus";
|
|
293
|
+
|
|
294
|
+
// Keep what is safe to send. An event also has `payload` and `error`.
|
|
295
|
+
const withoutPayload = ({ id, parentId, type, effect, phase }) => ({
|
|
296
|
+
id,
|
|
297
|
+
parentId,
|
|
298
|
+
type,
|
|
299
|
+
effect,
|
|
300
|
+
phase,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
window.addEventListener("error", () => {
|
|
304
|
+
const events = recorder.events; // a snapshot, oldest event first
|
|
305
|
+
const safe = events.map(withoutPayload);
|
|
306
|
+
const body = JSON.stringify(safe);
|
|
307
|
+
navigator.sendBeacon("/api/incident", body);
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
What to know before you send the events:
|
|
312
|
+
|
|
313
|
+
- **`payload` and `error` can hold private data**: what a user typed, tokens, personal text. The recorder keeps them as they are and does not redact. Choose what to send, as `withoutPayload` does.
|
|
314
|
+
- **There is no time on an event.** You get the order, not the duration. To add a time, wrap the recorder in a logger that stamps `performance.now()` on a copy of the event.
|
|
315
|
+
- **An old `parentId` can point at an event that was pushed out.** When you build a tree from the events, treat an unknown `parentId` as a root.
|
|
316
|
+
- **`recorder.events` is a snapshot with a `capacity`.** Each read builds a new array, so read it once at the moment you need it. Without a `capacity` it is one live array.
|
|
317
|
+
- **A low `sample` thins the buffer.** `createTracer(recorder, { sample: 0.05 })` records 5% of root tasks, so the buffer may hold little when the incident comes. See [Cut volume in production](#cut-volume-in-production).
|
|
318
|
+
- **A `capacity` that is not a positive integer** (`0`, a negative number, `NaN`) means no limit.
|
|
319
|
+
|
|
249
320
|
### Configure Redux DevTools
|
|
250
321
|
|
|
322
|
+
<!-- docs-check: new -->
|
|
323
|
+
|
|
251
324
|
Read-only (the bus owns no state, so there is no time travel). Does nothing when the extension is not installed. Has its own instance in the DevTools picker, apart from your store.
|
|
252
325
|
|
|
253
326
|
Quiet by default, so you see what happened before you see how:
|
|
@@ -311,6 +384,58 @@ reduxDevtools({
|
|
|
311
384
|
|
|
312
385
|
With two quick `search` emits and a `latest` handler, the default list is `search`, `search`. With `policies: true` it adds the `latest` stages of each call, for example `search/latest/cancel` for the first one.
|
|
313
386
|
|
|
387
|
+
### Find loops with `graphLogger`
|
|
388
|
+
|
|
389
|
+
Two handlers that emit each other's events never stop. `graphLogger` is a logger that finds such a loop while the app runs, writes a report once for it, and keeps the map of the events so you can draw it.
|
|
390
|
+
|
|
391
|
+
```js
|
|
392
|
+
// bus.js
|
|
393
|
+
import { createBus } from "nanoeffects";
|
|
394
|
+
import { createTracer, graphLogger, tracePlugin } from "nanoeffects/trace";
|
|
395
|
+
|
|
396
|
+
// graphLogger — the map of events and the loop finder, in one logger
|
|
397
|
+
export const graph = graphLogger({ name: "app" });
|
|
398
|
+
|
|
399
|
+
// createTracer — sends every task of the bus to the logger
|
|
400
|
+
const trace = createTracer(graph);
|
|
401
|
+
|
|
402
|
+
// tracePlugin — also reports each emit; the logger works without it, with a coarser map
|
|
403
|
+
export const bus = createBus({ trace }).use(tracePlugin());
|
|
404
|
+
|
|
405
|
+
// graph.cycles() lists the loops found; graph.toMermaid() draws the map
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
It reports a node that is in one chain three times, a storm of emits from outside any handler, and a chain that starts too many tasks. It writes to `console.warn`, or to `onCycle`, or to Redux DevTools with `devtools: true`. Nothing is stopped: to cut a loop use [`loopGuardPlugin`](loop-guard.md). The full walk-through, with scenarios you can run, is [Find and cut event loops](debugging-loops.md).
|
|
409
|
+
|
|
410
|
+
### Send safe data to a tool
|
|
411
|
+
|
|
412
|
+
A payload or an error can be anything: a class instance, a `Map`, a circular object, a function, an `Error` with a `cause`. A tool that gets such a value as it is can fail or show nothing. The built-in loggers make sure that what leaves your process is plain data:
|
|
413
|
+
|
|
414
|
+
| Logger | What it does with a payload and an error |
|
|
415
|
+
| ------ | ---------------------------------------- |
|
|
416
|
+
| `reduxDevtools()` | Makes both plain data with `safeSerialize` before the extension gets them. An `Error` is sent as its message. `serialize` changes this. |
|
|
417
|
+
| `performanceLogger()` | Sends ids and the phase; an error is text. A payload is not sent. |
|
|
418
|
+
| `consoleLogger()` | Passes the value to `console`, which prints any value and keeps it open for you to inspect. |
|
|
419
|
+
| `createTraceRecorder()` | Keeps the events as they are. `recorder.toJSON()` and `JSON.stringify(recorder)` give plain data. |
|
|
420
|
+
| `graphLogger()` | Exports no payload unless you ask, and then makes it plain. |
|
|
421
|
+
|
|
422
|
+
`safeSerialize(value)` is exported for your own logger. It never throws and never loops: an `Error` keeps its name, message, stack and cause, a cycle becomes `"[Circular]"`, a `Map` becomes `{ $map }`, a `Set` becomes `{ $set }`, an instance of a class keeps its own fields and its name as `$class`, and a getter is never called. A value that is too deep or too big is cut.
|
|
423
|
+
|
|
424
|
+
```js
|
|
425
|
+
import { createTracer, reduxDevtools, safeSerialize } from "nanoeffects/trace";
|
|
426
|
+
|
|
427
|
+
const trace = createTracer(
|
|
428
|
+
reduxDevtools({
|
|
429
|
+
name: "app",
|
|
430
|
+
// serialize — runs on every payload and error before the extension gets it;
|
|
431
|
+
// hide a secret first, then make the rest plain
|
|
432
|
+
serialize: (value) => safeSerialize(value, { maxDepth: 4 }),
|
|
433
|
+
}),
|
|
434
|
+
);
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
`serialize: false` sends values as they are, which is faster and can fail on a circular one.
|
|
438
|
+
|
|
314
439
|
### Write your own logger
|
|
315
440
|
|
|
316
441
|
```ts
|
package/docs/typescript.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Use nanoeffects in a TypeScript app with many files
|
|
2
2
|
|
|
3
|
+
> **Level D.** Read first: [Guide](guide.md#10-add-types-when-you-want-checking-optional).
|
|
4
|
+
|
|
3
5
|
Read this page if your app is in TypeScript and the handlers and modules live in different files. It shows how to write an action with a handler so that TypeScript infers its payload and result, and what to add when the handlers need the fields of your bus (`ctx.store`, the effects of `effectsPlugin`). For one file or for JavaScript you do not need it.
|
|
4
6
|
|
|
5
7
|
## Write an action with its handler
|
|
@@ -23,7 +25,7 @@ export const getUser = createAction(
|
|
|
23
25
|
},
|
|
24
26
|
);
|
|
25
27
|
|
|
26
|
-
// createModule: m.use binds the handler the
|
|
28
|
+
// createModule: m.use binds the handler the action carries, while the module is mounted
|
|
27
29
|
export const userModule = createModule("user", (m) => {
|
|
28
30
|
m.use(getUser);
|
|
29
31
|
});
|
package/docs/vue.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Use nanoeffects in a Vue app
|
|
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 your app is in Vue 3 and a component has to talk to the bus: tell it that a value changed, and show the result of an event. `vuePlugin` gives you two functions for that. To see them in a running project, start from the `vue-ts` template in [Start a project from a template](starter.md#start-with-vue).
|
|
4
6
|
|
|
5
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.
|
|
@@ -92,5 +94,5 @@ In TypeScript the type of the `ref` comes from the event: with `createAction<str
|
|
|
92
94
|
## Where to go next
|
|
93
95
|
|
|
94
96
|
- The same two jobs for React: [Build a sign-up form in React](react.md).
|
|
95
|
-
- A policy in a component (a debounced save): in `<script setup>` make it once with [`withPolicy`](policies.md#
|
|
97
|
+
- A policy in a component (a debounced save): in `<script setup>` make it once with [`withPolicy`](policies.md#withpolicy-call-a-policy-outside-a-bus); Vue runs the setup once, so it needs no hook.
|
|
96
98
|
- Keep values in Pinia: [Connect Redux, Zustand, MobX, or Pinia](stores.md).
|