nanoeffects 0.29.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-jeEqIKgK.d.ts → action-creator-6rlBTmH9.d.ts} +27 -2
- package/dist/actions.d.ts +3 -3
- package/dist/actions.js +1 -1
- package/dist/{bus-0uYn_kRj.js → bus-DFlmNJ6h.js} +1 -1
- package/dist/context-B5Z2RpJt.js +1 -0
- package/dist/{context-types-Bme1mzkn.d.ts → context-types-DKNc5mkS.d.ts} +1 -1
- package/dist/context.d.ts +2 -2
- 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 +1 -1
- package/dist/index.js +1 -1
- package/dist/match.d.ts +1 -1
- package/dist/{module-BWMW83qk.d.ts → module-G39MaVOt.d.ts} +2 -2
- package/dist/notrace/{action-creator-xpQ_I2h8.d.ts → action-creator-BrMSO3RP.d.ts} +1 -0
- package/dist/notrace/actions.d.ts +2 -2
- package/dist/notrace/effects.d.ts +2 -2
- package/dist/notrace/index.d.ts +1 -1
- package/dist/notrace/{plugin-6LhNx0th.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 +1 -1
- package/dist/oop.d.ts +1 -1
- package/dist/oop.js +1 -1
- package/dist/{plugin-BaErevLy.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 +2 -2
- package/dist/signal.d.ts +2 -2
- 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 +151 -2
- package/dist/trace.js +3 -1
- package/dist/{types-D5xsR37j.d.ts → types-N_cdRr1r.d.ts} +2 -2
- package/dist/vue.d.ts +1 -1
- package/dist/{with-policy-C6BIiPIi.js → with-policy-CeM1rtES.js} +1 -1
- package/dist/{with-policy-BZVRBUOm.d.ts → with-policy-CthiBp_z.d.ts} +1 -1
- package/docs/README.md +62 -31
- package/docs/actions.md +44 -27
- package/docs/cancel-stale-requests.md +2 -0
- package/docs/chat-example.md +3 -1
- package/docs/chat-widget.md +2 -0
- package/docs/context.md +321 -9
- package/docs/debounce-and-abort.md +3 -1
- package/docs/debugging-loops.md +283 -0
- package/docs/effects.md +255 -138
- package/docs/errors.md +4 -2
- package/docs/examples.md +60 -0
- package/docs/guide.md +39 -22
- package/docs/hmr.md +35 -1
- package/docs/loop-guard.md +109 -0
- package/docs/migrating-from-effector.md +4 -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 +2 -0
- package/docs/oop.md +3 -1
- package/docs/package.md +4 -1
- package/docs/pass-the-signal.md +281 -0
- package/docs/plugins.md +22 -5
- package/docs/policies.md +73 -46
- package/docs/prevent-double-submit.md +3 -1
- package/docs/query-libraries.md +3 -1
- package/docs/react.md +42 -13
- 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 +2 -0
- package/docs/server.md +2 -0
- package/docs/starter.md +2 -0
- package/docs/stores.md +2 -0
- package/docs/svelte.md +3 -1
- package/docs/tracing.md +58 -1
- package/docs/typescript.md +2 -0
- package/docs/vue.md +3 -1
- package/llms-full.txt +2008 -440
- package/llms.txt +11 -8
- package/package.json +22 -5
- package/dist/context-DtUnzC-1.js +0 -1
- package/docs/scope.md +0 -278
- package/docs/stability.md +0 -90
package/docs/ru/react.md
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# Форма регистрации на React
|
|
2
|
+
|
|
3
|
+
> **[Уровень B](README.md#виды-проектов)**: SPA с async UI. Сначала прочитайте: [Руководство](guide.md#1-создание-шины-событий).
|
|
4
|
+
|
|
5
|
+
Задача: форма регистрации. Пока пользователь вводит имя, форма спрашивает backend, свободно ли оно, а кнопка «Зарегистрироваться» отправляет форму. Из-за этого запросы начинают гоняться друг за другом: ответ на старый ввод может прийти позже ответа на новый и показать неверный результат. Страница показывает, как решить это с nanoeffects. Шина живёт вне React, компонент только подписывается на неё.
|
|
6
|
+
|
|
7
|
+
Шаги 1 и 2 обязательные, шаг 3 нет. Два последних раздела дополнительные.
|
|
8
|
+
|
|
9
|
+
Нужен проект на React с установленной `nanoeffects`. Форма общается с вашим клиентом, `api` ниже: замените его своими запросами.
|
|
10
|
+
|
|
11
|
+
## На этой странице
|
|
12
|
+
|
|
13
|
+
- [Проблемы формы без шины](#проблемы-формы-без-шины)
|
|
14
|
+
- [1. Шина и обработчики](#1-шина-и-обработчики)
|
|
15
|
+
- [2. Компонент формы](#2-компонент-формы)
|
|
16
|
+
- [3. Остановка запросов при закрытии формы](#3-остановка-запросов-при-закрытии-формы)
|
|
17
|
+
- [Последнее значение события как state](#последнее-значение-события-как-state)
|
|
18
|
+
- [Обработчик с debounce между рендерами](#обработчик-с-debounce-между-рендерами)
|
|
19
|
+
|
|
20
|
+
## Проблемы формы без шины
|
|
21
|
+
|
|
22
|
+
У формы регистрации три проблемы, которые `useEffect` и `fetch` не решают:
|
|
23
|
+
|
|
24
|
+
- **Гонка при проверке имени.** Пользователь печатает `ann`, затем `anna`, и на каждый ввод уходит запрос «свободно ли имя». Ответ для `ann` приходит последним и говорит, что имя свободно, хотя `anna` занято.
|
|
25
|
+
- **Повторная отправка.** Второй клик по «Зарегистрироваться» отправляет второй запрос, пока идёт первый.
|
|
26
|
+
- **Ненадёжная сеть.** Один упавший запрос нужно повторить, а ответ «имя занято» — нет.
|
|
27
|
+
|
|
28
|
+
У каждой есть политика или эффект: `latest`, `leading`, `retry`. Они живут в одном месте, вне компонента.
|
|
29
|
+
|
|
30
|
+
## 1. Шина и обработчики
|
|
31
|
+
|
|
32
|
+
Шина лежит в отдельном файле, а обработчики формы — в другом. Создайте шину один раз, вне любого компонента: шина, созданная в теле компонента, была бы новой шиной без подписчиков на каждом рендере.
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
// bus.js
|
|
36
|
+
import { createBus } from "nanoeffects";
|
|
37
|
+
|
|
38
|
+
export const bus = createBus(); // шина: создаётся один раз, в начале модуля
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Обработчики знают шину, а шина ничего не знает о них. Они регистрируются, когда импортируется `registration.js`, один раз, при старте приложения:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
// registration.js
|
|
45
|
+
import { retry } from "nanoeffects/effects";
|
|
46
|
+
import { debounce, latest, leading } from "nanoeffects/policies";
|
|
47
|
+
import { bus } from "./bus";
|
|
48
|
+
|
|
49
|
+
// fetch отклоняется с TypeError при сбое сети; HTTP-ошибка — обычный Error
|
|
50
|
+
const isNetworkError = (error) => error instanceof TypeError;
|
|
51
|
+
|
|
52
|
+
// debounce — подождать 300 мс тишины; latest — прервать проверку предыдущего имени
|
|
53
|
+
bus.on(
|
|
54
|
+
"username/typed",
|
|
55
|
+
debounce(
|
|
56
|
+
300,
|
|
57
|
+
latest(async (name, ctx) => {
|
|
58
|
+
// api — ваш клиент; передайте ctx.signal, чтобы latest мог отменить сам запрос
|
|
59
|
+
const available = await api.usernameAvailable(name, { signal: ctx.signal });
|
|
60
|
+
ctx.emit("username/checked", { name, available });
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// leading — второй клик игнорируется, пока идёт первая регистрация
|
|
66
|
+
bus.on(
|
|
67
|
+
"form/submitted",
|
|
68
|
+
leading(async (form, ctx) => {
|
|
69
|
+
const signUp = () => api.signUp(form, { signal: ctx.signal });
|
|
70
|
+
try {
|
|
71
|
+
// retry — до 3 попыток с паузой 500 мс и только если упала сеть
|
|
72
|
+
const user = await retry(ctx, signUp, {
|
|
73
|
+
times: 3,
|
|
74
|
+
delay: 500,
|
|
75
|
+
shouldRetry: isNetworkError,
|
|
76
|
+
});
|
|
77
|
+
ctx.emit("form/succeeded", user);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
ctx.emit("form/failed", error.message);
|
|
80
|
+
}
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Обработчики ничего не знают о React. Тест может отправить `username/typed` и прочитать `username/checked`, ничего не рендеря: ту же связку проверяют [`examples/registration-form.ts`](../../examples/registration-form.ts) и `src/tests/registration-form.test.ts`.
|
|
86
|
+
|
|
87
|
+
## 2. Компонент формы
|
|
88
|
+
|
|
89
|
+
<!-- docs-check: continue -->
|
|
90
|
+
|
|
91
|
+
Компонент хранит то, что показывает, в `useState`. Он подписывается в `useEffect` и отправляет события из своих обработчиков. `bus.on` возвращает функцию отписки; если вернуть её из эффекта, подписка снимется при размонтировании. Если у компонента уже есть `AbortSignal`, `bus.on(event, handler, { signal })` заканчивает подписку вместе с ним; см. [`signalPlugin`](plugins.md#внешний-сигнал-для-emit-и-подписки-signalplugin).
|
|
92
|
+
|
|
93
|
+
```jsx
|
|
94
|
+
// SignUpForm.jsx
|
|
95
|
+
import { useEffect, useState } from "react";
|
|
96
|
+
import { bus } from "./bus"; // шина из предыдущего фрагмента
|
|
97
|
+
import "./registration"; // регистрирует обработчики; приложение делает это один раз, при старте
|
|
98
|
+
|
|
99
|
+
export function SignUpForm() {
|
|
100
|
+
const [name, setName] = useState("");
|
|
101
|
+
const [password, setPassword] = useState("");
|
|
102
|
+
const [available, setAvailable] = useState(null); // null: ещё не проверено
|
|
103
|
+
const [status, setStatus] = useState("idle"); // idle, sending, done или failed
|
|
104
|
+
const [error, setError] = useState("");
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
// bus.on — слушать то, о чём сообщают обработчики; каждый вызов возвращает функцию отписки
|
|
108
|
+
const unbindChecked = bus.on("username/checked", (result) => {
|
|
109
|
+
setAvailable(result.available);
|
|
110
|
+
});
|
|
111
|
+
const unbindSucceeded = bus.on("form/succeeded", () => setStatus("done"));
|
|
112
|
+
const unbindFailed = bus.on("form/failed", (message) => {
|
|
113
|
+
setStatus("failed");
|
|
114
|
+
setError(message);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return () => {
|
|
118
|
+
unbindChecked();
|
|
119
|
+
unbindSucceeded();
|
|
120
|
+
unbindFailed();
|
|
121
|
+
};
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
124
|
+
function handleName(event) {
|
|
125
|
+
const value = event.target.value;
|
|
126
|
+
setName(value);
|
|
127
|
+
setAvailable(null);
|
|
128
|
+
bus.emit("username/typed", value); // bus.emit — обработчик делает debounce и проверяет имя
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function handleSubmit(event) {
|
|
132
|
+
event.preventDefault();
|
|
133
|
+
setStatus("sending");
|
|
134
|
+
bus.emit("form/submitted", { name, password }); // второй клик отбрасывает leading
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<form onSubmit={handleSubmit}>
|
|
139
|
+
<input value={name} onChange={handleName} placeholder="Username" />
|
|
140
|
+
{available === false && <p>This name is taken</p>}
|
|
141
|
+
<input
|
|
142
|
+
type="password"
|
|
143
|
+
value={password}
|
|
144
|
+
onChange={(event) => setPassword(event.target.value)}
|
|
145
|
+
/>
|
|
146
|
+
<button type="submit" disabled={status === "sending"}>Sign up</button>
|
|
147
|
+
{status === "failed" && <p>{error}</p>}
|
|
148
|
+
{status === "done" && <p>Welcome!</p>}
|
|
149
|
+
</form>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Кнопка `disabled` нужна только для вида. Форма остаётся корректной и без неё: `leading` отбрасывает вторую отправку, пока идёт первая.
|
|
155
|
+
|
|
156
|
+
При разработке React StrictMode запускает эффект дважды (подписка, отписка, подписка). Здесь это безопасно, потому что очистка снимает ровно то, что добавил эффект.
|
|
157
|
+
|
|
158
|
+
## 3. Остановка запросов при закрытии формы
|
|
159
|
+
|
|
160
|
+
<!-- docs-check: continue -->
|
|
161
|
+
|
|
162
|
+
Шаг необязательный. Отписка не даёт компоненту слышать результаты, но начавшийся запрос продолжает выполняться. Чтобы его отменить, компонент отправляет `form/closed` при размонтировании, а каждый обработчик, который должен остановиться, слушает его через `ctx.on`:
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
import { latest } from "nanoeffects/policies";
|
|
166
|
+
|
|
167
|
+
// проверка имени из registration.js, с одной новой строкой
|
|
168
|
+
const checkName = latest(async (name, ctx) => {
|
|
169
|
+
// ctx.on — подписка этой задачи; прерывание задачи заодно отбрасывает результат
|
|
170
|
+
ctx.on("form/closed", () => ctx.abort());
|
|
171
|
+
const available = await api.usernameAvailable(name, { signal: ctx.signal });
|
|
172
|
+
ctx.emit("username/checked", { name, available });
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
В компоненте добавьте `bus.emit("form/closed")` в функцию очистки эффекта. Это необязательно: пропустите, если завершение запроса после размонтирования ничему не вредит, как для дешёвой проверки.
|
|
177
|
+
|
|
178
|
+
## Последнее значение события как state
|
|
179
|
+
|
|
180
|
+
**Проблема.** Компоненту часто нужен последний результат события, например найденные элементы поиска. Рукописную форму легко написать с ошибкой:
|
|
181
|
+
|
|
182
|
+
<!-- docs-check: skip - рукописная форма, которую заменяет этот раздел -->
|
|
183
|
+
```jsx
|
|
184
|
+
useEffect(() => bus.on(searchLoaded, setFound), []);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Рядом нужен `useState`, пустой `[]` скрывает зависимость от события, и всё ломается, если payload — функция, потому что React вызывает `setFound(payload)` как функцию-обновитель.
|
|
188
|
+
|
|
189
|
+
**Когда.** Значение, которое вы показываете, приходит с шины, а не из компонента. Для обратного направления (значение компонента должна услышать шина) берите `useEmitter`.
|
|
190
|
+
|
|
191
|
+
`useEventState(event, initial)` имеет сигнатуру `useState`: возвращает `[state, setState]`. `initial` — state до первого события, и это может быть функция, как в `useState`. После этого каждый `emit` события `event` устанавливает state в его payload. Хук отписывается, когда компонент размонтируется.
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
// bus.js
|
|
195
|
+
import { createBus } from "nanoeffects";
|
|
196
|
+
import { reactPlugin } from "nanoeffects/react";
|
|
197
|
+
|
|
198
|
+
// createBus — шина событий, создаётся один раз вне React
|
|
199
|
+
// reactPlugin — добавляет хуки bus.useEmitter и bus.useEventState
|
|
200
|
+
export const bus = createBus().use(reactPlugin());
|
|
201
|
+
export const { useEventState } = bus; // свободный хук: компоненты импортируют его без шины
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
```js
|
|
205
|
+
// search.js
|
|
206
|
+
import { createAction } from "nanoeffects/actions";
|
|
207
|
+
|
|
208
|
+
// createAction — имя события; обработчики отправляют его с найденными заголовками
|
|
209
|
+
export const searchLoaded = createAction("search/loaded");
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
```jsx
|
|
213
|
+
// Found.jsx
|
|
214
|
+
import { useEventState } from "./bus";
|
|
215
|
+
import { searchLoaded } from "./search";
|
|
216
|
+
|
|
217
|
+
export function Found() {
|
|
218
|
+
// useEventState — как useState([]), но значение устанавливает searchLoaded
|
|
219
|
+
const [found, setFound] = useEventState(searchLoaded, []);
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<>
|
|
223
|
+
<ul>
|
|
224
|
+
{found.map((title) => (
|
|
225
|
+
<li key={title}>{title}</li>
|
|
226
|
+
))}
|
|
227
|
+
</ul>
|
|
228
|
+
<button onClick={() => setFound([])}>Clear</button>
|
|
229
|
+
</>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`setFound` — сеттер React. Он меняет state только этого компонента и ничего не отправляет.
|
|
235
|
+
|
|
236
|
+
`useEventState` — не store. Каждый компонент, который его вызывает, хранит свою копию последней payload, а компонент, смонтированный позже, ранних событий не видит. Чтобы делить значение между компонентами или сохранить его для тех, что смонтируются позже, держите его в state manager (Redux, Zustand, Jotai или другом) и записывайте в него из обработчика события. См. [State managers](stores.md). Без state manager один `useEventState` в провайдере Context в корне делится значением со всеми компонентами ниже: [Чистый React: Context, `useEmitter` и `useEventState`](stores.md#чистый-react-context-useemitter-и-useeventstate).
|
|
237
|
+
|
|
238
|
+
В TypeScript тип state берётся из события: из `createAction<string[]>("search/loaded")` или из карты событий шины. Когда у события нет типа payload, как в примере выше, он берётся из `initial`: `["a"]` — это `string[]`, а пустой `[]` — `unknown[]`. Если нужно больше, задайте событию тип или передайте типизированный `initial`.
|
|
239
|
+
|
|
240
|
+
Шина не хранит значения, поэтому хук не может воспроизвести событие заново. Событие, отправленное до монтирования компонента, не видно: если это важно, прочитайте текущее значение в `initial`.
|
|
241
|
+
|
|
242
|
+
## Обработчик с debounce между рендерами
|
|
243
|
+
|
|
244
|
+
**Проблема.** Компонент запускается заново при каждом рендере. Политика, созданная в его теле, например `debounce(300, search)`, создаётся заново каждый раз и теряет свой таймер. Политика, созданная один раз, держит callback первого рендера, который читает старые props и state.
|
|
245
|
+
|
|
246
|
+
**Когда.** Обработчику в компоненте нужна политика, а шина вам не нужна: поле поиска, автосохранение, отправка, которая не должна сработать дважды.
|
|
247
|
+
|
|
248
|
+
`usePolicy(callback, make, deps)` держит одну функцию между рендерами. Она вызывает `callback` последнего рендера, так что встроенный callback видит свежее state. При размонтировании компонента она отменяет ожидающие вызовы.
|
|
249
|
+
|
|
250
|
+
```jsx
|
|
251
|
+
import { useState } from "react";
|
|
252
|
+
import { debounce } from "nanoeffects/policies";
|
|
253
|
+
import { usePolicy } from "nanoeffects/react";
|
|
254
|
+
|
|
255
|
+
export const Search = ({ wait }) => {
|
|
256
|
+
const [items, setItems] = useState([]);
|
|
257
|
+
|
|
258
|
+
// usePolicy — стабильная функция; первый аргумент — callback последнего рендера
|
|
259
|
+
const onChange = usePolicy(
|
|
260
|
+
async (query, { signal }) => {
|
|
261
|
+
const found = await api.search(query, { signal });
|
|
262
|
+
setItems(found);
|
|
263
|
+
},
|
|
264
|
+
// debounce(wait) — второй аргумент один раз строит политику вокруг callback выше
|
|
265
|
+
debounce(wait),
|
|
266
|
+
// deps — перечислите то, что читает второй аргумент; новый wait создаёт новую политику
|
|
267
|
+
[wait],
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<div>
|
|
272
|
+
<input onChange={(event) => onChange(event.target.value)} />
|
|
273
|
+
<p>{items.length} found</p>
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
};
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
- `onChange` сохраняет идентичность, пока `wait` не меняется, так что его безопасно использовать в `useEffect` и props.
|
|
280
|
+
- Ожидающий вызов отменяется при размонтировании и при смене `deps`.
|
|
281
|
+
- Пишите политику без её callback: `debounce(300)`, а не `debounce(300, callback)`. `usePolicy` подставляет callback сам, всегда callback последнего рендера.
|
|
282
|
+
- Чтобы объединить политики, напишите функцию от задачи: `(task) => throttle(100, latest(task))`. Здесь `task` — callback, который `usePolicy` держит актуальным; передавайте его, а не свою функцию.
|
|
283
|
+
- `deps` остаются на вас: `debounce(wait)` создаётся заново при каждом рендере, но используется только первый (и тот, что после смены `deps`).
|
|
284
|
+
- Вне компонента React берите [`withPolicy`](policies.md#withpolicy-вызов-политики-вне-шины).
|
|
285
|
+
|
|
286
|
+
### Политика без функции
|
|
287
|
+
|
|
288
|
+
**Проблема.** `usePolicy` сам подставляет в политику callback последнего рендера, поэтому ему нужна политика, у которой ещё нет функции. Если бы вы передавали туда свою функцию, легко было бы передать не ту.
|
|
289
|
+
|
|
290
|
+
Опустите функцию, и политика вернёт обёртку. Обёртка принимает функцию и возвращает ту же политику, что и прямой вызов:
|
|
291
|
+
|
|
292
|
+
```js
|
|
293
|
+
import { debounce } from "nanoeffects/policies";
|
|
294
|
+
|
|
295
|
+
const runSearch = async (query) => {
|
|
296
|
+
await api.search(query);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// debounce(300) — функции ещё нет: возвращает обёртку
|
|
300
|
+
const wrap = debounce(300);
|
|
301
|
+
|
|
302
|
+
// обёртка принимает функцию; то же, что debounce(300, runSearch)
|
|
303
|
+
const search = wrap(runSearch);
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
- Так работают `latest()`, `leading()`, `debounce(ms)`, `throttle(ms)` и `queue()`. Параметры идут последним аргументом: `queue({ concurrency: 2 })`, `debounce(300, { leading: true })`.
|
|
307
|
+
- Каждое применение обёртки к функции даёт новую политику со своим state.
|
|
308
|
+
- Чтобы объединить политики, вкладывайте их: `throttle(100, latest(fn))`.
|
|
309
|
+
- В TypeScript с `key` аннотируйте payload в функции ключа: `latest({ key: (user: User) => user.id })`. Без аннотации обёртка не знает тип payload.
|