nanoeffects 0.20.0 → 0.22.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/dist/actions.d.ts +2 -2
- package/dist/dev.d.ts +1 -1
- package/dist/effects.d.ts +1 -1
- package/dist/effects.js +1 -1
- package/dist/{module-B0KVDNjY.d.ts → module-1sUEXIcD.d.ts} +1 -1
- package/dist/notrace/actions.d.ts +1 -1
- package/dist/notrace/effects.d.ts +1 -1
- package/dist/notrace/effects.js +1 -1
- package/dist/notrace/{plugin-C6KA4dX-.d.ts → plugin-ByDuI8Vq.d.ts} +20 -3
- package/dist/notrace/policies.d.ts +50 -5
- package/dist/notrace/policies.js +1 -1
- package/dist/{plugin-D9UTzGRG.d.ts → plugin-BiGv84Xu.d.ts} +20 -3
- package/dist/policies.d.ts +50 -5
- package/dist/policies.js +1 -1
- package/dist/react.d.ts +6 -4
- package/dist/scope.d.ts +1 -1
- package/dist/svelte.d.ts +46 -0
- package/dist/svelte.js +1 -0
- package/dist/{types-CbdJKC2z.d.ts → types-DC-aLJit.d.ts} +1 -1
- package/docs/README.md +2 -0
- package/docs/chat-example.md +8 -3
- package/docs/effects.md +8 -1
- package/docs/migrating-from-saga.md +3 -1
- package/docs/package.md +1 -0
- package/docs/policies.md +27 -0
- package/docs/query-libraries.md +2 -0
- package/docs/react.md +5 -3
- package/docs/starter.md +12 -1
- package/docs/stores.md +1 -1
- package/docs/svelte.md +81 -0
- package/llms-full.txt +153 -11
- package/llms.txt +3 -2
- package/package.json +20 -2
package/docs/README.md
CHANGED
|
@@ -19,6 +19,7 @@ Code for every guide step: [`examples/guide.ts`](../examples/guide.ts) (type-che
|
|
|
19
19
|
| Already on Redux / Zustand / MobX / Pinia | [Stores](stores.md) |
|
|
20
20
|
| A React form with async checks | [Build a sign-up form in React](react.md) |
|
|
21
21
|
| A Vue app | [Use nanoeffects in a Vue app](vue.md) |
|
|
22
|
+
| A Svelte app | [Use nanoeffects in a Svelte app](svelte.md) |
|
|
22
23
|
| Old responses overwrite new ones | [Cancel stale requests](cancel-stale-requests.md) |
|
|
23
24
|
| Debounced input that must also cancel the old request | [Debounce and abort](debounce-and-abort.md) |
|
|
24
25
|
| A second click sends a second request | [Prevent a double submit](prevent-double-submit.md) |
|
|
@@ -34,6 +35,7 @@ Code for every guide step: [`examples/guide.ts`](../examples/guide.ts) (type-che
|
|
|
34
35
|
- [Actions, commands, jobs and modules](actions.md)
|
|
35
36
|
- [Sign-up form in React](react.md) — one form: races, double submit, retry
|
|
36
37
|
- [nanoeffects in a Vue app](vue.md) — `watchEmitter` and `useEventRef`
|
|
38
|
+
- [nanoeffects in a Svelte app](svelte.md) — `eventStore`
|
|
37
39
|
- [Context fields](context.md) — store or client on every task
|
|
38
40
|
- [Use the bus from a class](oop.md) — `extendBus`, `@WithBus`, `@On`, and `dispose()` for the subscriptions of an instance
|
|
39
41
|
- [TypeScript in many files](typescript.md) — keep the types of the bus in commands and modules
|
package/docs/chat-example.md
CHANGED
|
@@ -44,7 +44,7 @@ Each domain is a folder. The components only emit events and read values; they n
|
|
|
44
44
|
| `src/domains/incoming/` | New messages, asked for at an interval. |
|
|
45
45
|
| `src/ui/` | The components. |
|
|
46
46
|
|
|
47
|
-
A domain folder has its events (`rooms.ts`, which imports no bus), its work (`rooms-handlers.ts`), the state it keeps (`rooms-store.ts`) and a module (`rooms-module.ts`) that wires them. A domain reacts to another domain by listening to its events, never by importing its handlers or its store.
|
|
47
|
+
A domain folder has its events (`rooms.ts`, which imports no bus), its work (`rooms-handlers.ts`), the state it keeps (`rooms-store.ts`) and a module (`rooms-module.ts`) that wires them. A domain reacts to another domain by listening to its events, never by importing its handlers or its store. Events and commands both live in the events file. An event (`createAction`) says that something happened, and any domain may listen: `rooms/selected` is heard by three. A command (`createCommand`) is an order with exactly one handler, as `search/openResult` and `messages/mark` are. The handler is given to `createCommand`, so the command works wherever it is called and can return a result, and the module binds it with `module.use` for as long as it is mounted. A job is a command that also sends its outcomes, but its handler is bound in the module (`module.handle`): a job usually runs under a policy, and the type of its outcomes is exact only when the handler is not part of the type.
|
|
48
48
|
|
|
49
49
|
## Load a history that must not race
|
|
50
50
|
|
|
@@ -173,19 +173,24 @@ export const runSearch = debounce(
|
|
|
173
173
|
`take` waits for the next event of a name. Start it before the emit that causes the event, so nothing is missed:
|
|
174
174
|
|
|
175
175
|
```js
|
|
176
|
+
import { createCommand } from "nanoeffects/actions";
|
|
176
177
|
import { take } from "nanoeffects/effects";
|
|
177
178
|
|
|
179
|
+
// createCommand — an order for the messages domain, with the handler that does it
|
|
180
|
+
const markMessage = createCommand("messages/mark", (messageId) => console.log("mark", messageId));
|
|
181
|
+
|
|
178
182
|
// the room that is open, kept from the rooms/selected event
|
|
179
183
|
let selectedRoom = "general";
|
|
180
184
|
|
|
181
|
-
export const
|
|
185
|
+
export const goToResult = async (hit, ctx) => {
|
|
182
186
|
if (hit.roomId !== selectedRoom) {
|
|
183
187
|
// take — wait for the next event of that name; started before the emit so nothing is missed
|
|
184
188
|
const loaded = take(ctx, "messages/loadHistory/success");
|
|
185
189
|
ctx.emit("rooms/selected", hit.roomId);
|
|
186
190
|
await loaded;
|
|
187
191
|
}
|
|
188
|
-
ctx.emit
|
|
192
|
+
// ctx.emit with a command — the messages domain marks the message
|
|
193
|
+
ctx.emit(markMessage(hit.id));
|
|
189
194
|
};
|
|
190
195
|
```
|
|
191
196
|
|
package/docs/effects.md
CHANGED
|
@@ -244,18 +244,24 @@ bus.on("watch", (_, ctx) => {
|
|
|
244
244
|
- Under `latest`, subscriptions of a superseded run end with that run.
|
|
245
245
|
- `ctx.off(event)` clears every handler of the event on the bus, not only the task’s.
|
|
246
246
|
|
|
247
|
-
`take(ctx, event, options?)` waits for the next `event` and resolves with its payload
|
|
247
|
+
`take(ctx, event, options?)` waits for the next `event` and resolves with its payload. `event` is the name of an event or an action creator, as for `bus.on`:
|
|
248
248
|
|
|
249
249
|
```js
|
|
250
250
|
import { createBus } from "nanoeffects";
|
|
251
|
+
import { createAction } from "nanoeffects/actions";
|
|
251
252
|
import { take, timeout } from "nanoeffects/effects";
|
|
252
253
|
|
|
253
254
|
const bus = createBus();
|
|
255
|
+
const stopRequested = createAction("stop"); // an event with a name; calling it builds the action
|
|
254
256
|
|
|
255
257
|
bus.on("login", async (user, ctx) => {
|
|
256
258
|
const reason = await take(ctx, "logout"); // resolves on the next logout
|
|
257
259
|
cleanup(user, reason);
|
|
258
260
|
|
|
261
|
+
// an action creator works the same, and its payload type is the type of the result
|
|
262
|
+
const stopped = await take(ctx, stopRequested);
|
|
263
|
+
console.log(stopped);
|
|
264
|
+
|
|
259
265
|
// wait for a matching event, but not longer than 5 s
|
|
260
266
|
const id = await timeout(ctx, 5000, ({ signal }) =>
|
|
261
267
|
take(ctx, "saved", { signal, predicate: (savedId) => savedId === draft.id }),
|
|
@@ -264,6 +270,7 @@ bus.on("login", async (user, ctx) => {
|
|
|
264
270
|
```
|
|
265
271
|
|
|
266
272
|
- Sees only events emitted after the call, takes one, and unsubscribes.
|
|
273
|
+
- With an action creator the result has the payload type of the creator. A creator made without a payload type (`createAction("saved")`) takes it from the event map of the bus.
|
|
267
274
|
- `options.predicate` skips payloads it rejects. In TypeScript, a type guard (`(payload): payload is Admin`) narrows the type of the result.
|
|
268
275
|
- `options.signal` stops the wait (how a `race` or `timeout` starter cancels it).
|
|
269
276
|
- On abort of the task or of `options.signal`, unsubscribes and rejects with the abort reason.
|
|
@@ -14,7 +14,7 @@ Read this page if you know redux-saga and want to find the same job in nanoeffec
|
|
|
14
14
|
| `fork(fn, x)` | `ctx.run(task, x)` without `await` |
|
|
15
15
|
| `spawn(fn, x)` | `ctx.emit(event, x)` or `bus.emit(event, x)` |
|
|
16
16
|
| `put(action)` | `ctx.emit(action)` |
|
|
17
|
-
| `take(pattern)` | [`take(ctx, event, { predicate })`](effects.md) |
|
|
17
|
+
| `take(pattern)` | [`take(ctx, event, { predicate })`](effects.md), where `event` is a name or an action creator |
|
|
18
18
|
| `select` | a [context field](context.md), such as `store` |
|
|
19
19
|
| `all`, `race` | [`all`, `race`](effects.md) |
|
|
20
20
|
| `retry`, `delay` | [`retry`, `delay`](effects.md) |
|
|
@@ -62,6 +62,8 @@ bus.on(
|
|
|
62
62
|
|
|
63
63
|
Tasks are plain `async` functions. `ctx.signal` replaces `cancel`: pass it to `fetch` or to your client, and the request stops.
|
|
64
64
|
|
|
65
|
+
To see the counter, `takeLatest` with `delay`, a cancel and a failing call running, open the [counter example](../examples/counter): it has a page without a store and a page with Redux Toolkit.
|
|
66
|
+
|
|
65
67
|
## Start a task without waiting
|
|
66
68
|
|
|
67
69
|
`fork` and `spawn` differ in what happens when the caller is aborted. Pick by that.
|
package/docs/package.md
CHANGED
|
@@ -16,6 +16,7 @@ Imports are split so you only load what you use. `createBus` stays small; the re
|
|
|
16
16
|
| `nanoeffects/redux` | `createEffectsMiddleware` |
|
|
17
17
|
| `nanoeffects/observable` | `createObserve`, `Subject` |
|
|
18
18
|
| `nanoeffects/react` | `reactPlugin`: adds the `useEmitter` hook to a bus (needs React 18 or newer) |
|
|
19
|
+
| `nanoeffects/svelte` | `sveltePlugin`: adds `eventStore` to a bus (needs Svelte 5 or newer) |
|
|
19
20
|
| `nanoeffects/vue` | `vuePlugin`: adds `watchEmitter` and `useEventRef` to a bus (needs Vue 3.3 or newer) |
|
|
20
21
|
| `nanoeffects/dev` | `devPlugin` (no-op in production) |
|
|
21
22
|
| `nanoeffects/trace` | `createTracer`, the loggers and `tracePlugin` (names for the trace) |
|
package/docs/policies.md
CHANGED
|
@@ -36,6 +36,33 @@ Every policy accepts `key` in its options (see [Isolate state by key](#isolate-s
|
|
|
36
36
|
| `throttle(ms, fn, options?)` | at most one run per `ms` |
|
|
37
37
|
| `queue(fn, options?)` | one after another, in order |
|
|
38
38
|
|
|
39
|
+
## Call a policy without its function
|
|
40
|
+
|
|
41
|
+
**Problem.** Some code builds the function for you and only asks which policy to use, as [`usePolicy`](react.md#keep-a-debounced-handler-between-renders) does. If you must pass your own function there, it is easy to pass the wrong one.
|
|
42
|
+
|
|
43
|
+
**When.** You pass a policy to `usePolicy`. With a bus, `withPolicy` or your own code, call the policy with its function as above.
|
|
44
|
+
|
|
45
|
+
Leave the function out and the policy returns a wrapper. The wrapper takes a function and gives back the same policy as the direct call:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
import { debounce } from "nanoeffects/policies";
|
|
49
|
+
|
|
50
|
+
const runSearch = async (query) => {
|
|
51
|
+
await api.search(query);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// debounce(300) — no function yet: returns a wrapper
|
|
55
|
+
const wrap = debounce(300);
|
|
56
|
+
|
|
57
|
+
// the wrapper takes the function; same as debounce(300, runSearch)
|
|
58
|
+
const search = wrap(runSearch);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- It works for `latest()`, `leading()`, `debounce(ms)`, `throttle(ms)` and `queue()`, with the options as the last argument: `queue({ concurrency: 2 })`, `debounce(300, { leading: true })`.
|
|
62
|
+
- Each time you apply a wrapper to a function, you get a new policy with its own state.
|
|
63
|
+
- To combine policies, nest them as before: `throttle(100, latest(fn))`.
|
|
64
|
+
- With `key`, annotate the payload of the key function in TypeScript: `latest({ key: (user: User) => user.id })`. Without the annotation the wrapper cannot know the payload type.
|
|
65
|
+
|
|
39
66
|
## Configure debounce and throttle
|
|
40
67
|
|
|
41
68
|
`debounce` and `throttle` take `leading` and `trailing`. Behavior matches lodash (and patronum); tests compare random call sequences to lodash itself.
|
package/docs/query-libraries.md
CHANGED
|
@@ -122,3 +122,5 @@ Send it from the handler with `ctx.emit(userLoaded(user))`. Do not replace the c
|
|
|
122
122
|
- **A handler that returns without a promise is finished with the call**, so a signal that aborts later does not reach it. Commands that fetch are async, so this only matters for a sync handler that sets up a callback.
|
|
123
123
|
- **Give each request its own signal for an event.** For a command or a job the link ends when it settles. For `bus.emit(action, { signal })` it stays until the signal aborts.
|
|
124
124
|
- **The signal reaches the tasks that this `emit` starts**, with the listeners of the command's type and the `ctx.run` children. A separate `bus.emit` that the handler makes is not linked: pass `{ signal: ctx.signal }` to it.
|
|
125
|
+
|
|
126
|
+
A whole small app, with a search, a filter, pages and a detail panel, is in the [Rick and Morty example](../examples/rickandmorty).
|
package/docs/react.md
CHANGED
|
@@ -248,8 +248,8 @@ export const Search = ({ wait }) => {
|
|
|
248
248
|
const found = await api.search(query, { signal });
|
|
249
249
|
setItems(found);
|
|
250
250
|
},
|
|
251
|
-
// the second argument builds the policy once
|
|
252
|
-
|
|
251
|
+
// debounce(wait) — the second argument builds the policy once around the callback above
|
|
252
|
+
debounce(wait),
|
|
253
253
|
// deps — list what the second argument reads; a new wait makes a new policy
|
|
254
254
|
[wait],
|
|
255
255
|
);
|
|
@@ -265,7 +265,9 @@ export const Search = ({ wait }) => {
|
|
|
265
265
|
|
|
266
266
|
- `onChange` keeps its identity while `wait` stays the same, so it is safe in `useEffect` and in props.
|
|
267
267
|
- A pending call is cancelled on unmount and when `deps` change.
|
|
268
|
-
-
|
|
268
|
+
- Write the policy without its callback: `debounce(300)`, not `debounce(300, callback)`. `usePolicy` puts the callback in itself, always the one of the latest render.
|
|
269
|
+
- To combine policies, write a function of the task: `(task) => throttle(100, latest(task))`. Here `task` is the callback that `usePolicy` keeps current; pass it, not your own function.
|
|
270
|
+
- `deps` stay your job: `debounce(wait)` is made again on every render, but only the first one (and the one after `deps` change) is used.
|
|
269
271
|
- Without a React component, use [`withPolicy`](policies.md#use-a-policy-without-a-bus).
|
|
270
272
|
|
|
271
273
|
## Where to go next
|
package/docs/starter.md
CHANGED
|
@@ -48,6 +48,7 @@ Pass `--template` to skip the question. With npm, put `--` before the option.
|
|
|
48
48
|
| `react-ts` | The `react` app in TypeScript | You build a React app in TypeScript |
|
|
49
49
|
| `domains-ts` | Commands, a job and modules, with the logic split into domain folders and React as a thin layer | You build an app with more than one feature and want the logic off the UI |
|
|
50
50
|
| `vue-ts` | The `react-ts` app in Vue 3, with `watchEmitter` and `useEventRef` from `nanoeffects/vue` | You build a Vue 3 app |
|
|
51
|
+
| `svelte-ts` | The `react-ts` app in Svelte 5, with `eventStore` from `nanoeffects/svelte` | You build a Svelte app |
|
|
51
52
|
|
|
52
53
|
```bash
|
|
53
54
|
npm create nanoeffects@latest my-app -- --template react
|
|
@@ -71,6 +72,16 @@ npm create nanoeffects@latest my-app -- --template vue-ts
|
|
|
71
72
|
|
|
72
73
|
`npm run build` type-checks the `.vue` files with `vue-tsc`, which needs TypeScript 5: the template depends on it.
|
|
73
74
|
|
|
75
|
+
### Start with Svelte
|
|
76
|
+
|
|
77
|
+
`svelte-ts` is the search app of `react-ts` in Svelte 5. Its bus uses `sveltePlugin` from `nanoeffects/svelte`: `eventStore` makes a Svelte store from an event. In a component, `$found` reads it, and Svelte subscribes and unsubscribes for you. The query goes to the bus from the `oninput` handler, so nothing is emitted when the component mounts. The store listens only while something reads it, and is [not a store of the app](svelte.md#it-is-not-a-store-of-the-app): a value that several components share belongs in a store of your own.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm create nanoeffects@latest my-app -- --template svelte-ts
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`npm run build` type-checks the `.svelte` files with `svelte-check`, which needs TypeScript 5: the template depends on it.
|
|
84
|
+
|
|
74
85
|
### Start with domains
|
|
75
86
|
|
|
76
87
|
`domains-ts` is not the search box of the other templates grown in TypeScript: it shows where logic goes when an app has several features. Each domain is a folder under `src/domains/`. It has the events of the domain, the work that handles them, and a module that mounts the work. The React components only emit events and read values.
|
|
@@ -93,7 +104,7 @@ The commands, jobs and modules themselves are explained in [Actions, commands, j
|
|
|
93
104
|
|
|
94
105
|
## Read the files
|
|
95
106
|
|
|
96
|
-
Every template except `domains-ts` splits the code the way [the guide](guide.md) does: one job per file. In the TypeScript templates the files end in `.ts` (and `.tsx` for React components, `.vue` for Vue components).
|
|
107
|
+
Every template except `domains-ts` splits the code the way [the guide](guide.md) does: one job per file. In the TypeScript templates the files end in `.ts` (and `.tsx` for React components, `.vue` for Vue components, `.svelte` for Svelte components).
|
|
97
108
|
|
|
98
109
|
| File | Job |
|
|
99
110
|
| ---- | --- |
|
package/docs/stores.md
CHANGED
|
@@ -25,7 +25,7 @@ Redux’s `dispatch` → reducers updates **state**. It does not give you first-
|
|
|
25
25
|
|
|
26
26
|
`createEffectsMiddleware(bus)` does the same job: after the reducer runs, the action also hits the nanoeffects bus you pass to it. Keep RTK for state; use policies and effects for async.
|
|
27
27
|
|
|
28
|
-
Skip this section if you are not already on Redux. The recipe as a file, type-checked with the project: [`examples/redux.ts`](../examples/redux.ts).
|
|
28
|
+
Skip this section if you are not already on Redux. The recipe as a file, type-checked with the project: [`examples/redux.ts`](../examples/redux.ts). To run it, open the [counter example](../examples/counter): `redux.html` is a counter with Redux Toolkit and this middleware.
|
|
29
29
|
|
|
30
30
|
```js
|
|
31
31
|
// bus.js
|
package/docs/svelte.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Use nanoeffects in a Svelte app
|
|
2
|
+
|
|
3
|
+
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
|
+
|
|
5
|
+
You need Svelte 5 or newer and `nanoeffects` installed. If you do not know what the bus is, read the [Guide](guide.md) first.
|
|
6
|
+
|
|
7
|
+
## Add the plugin to the bus
|
|
8
|
+
|
|
9
|
+
**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.
|
|
10
|
+
|
|
11
|
+
**When.** A component shows the payload of an event, such as the result of a search.
|
|
12
|
+
|
|
13
|
+
`sveltePlugin()` adds `eventStore` to the bus. Take it out of the bus once, in the file that makes it, and import it where you need it:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
// bus.js
|
|
17
|
+
import { createBus } from "nanoeffects";
|
|
18
|
+
import { sveltePlugin } from "nanoeffects/svelte";
|
|
19
|
+
|
|
20
|
+
// createBus — the event bus, created once outside Svelte
|
|
21
|
+
// sveltePlugin — adds eventStore to the bus
|
|
22
|
+
export const bus = createBus().use(sveltePlugin());
|
|
23
|
+
export const { eventStore } = bus; // a free function: it does not read `this`
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Show the result of an event
|
|
27
|
+
|
|
28
|
+
<!-- docs-check: continue -->
|
|
29
|
+
|
|
30
|
+
`eventStore(event, initial)` returns a Svelte store. Its value is `initial` at first and the payload of every `event` after that. In a component, `$found` reads it, and Svelte subscribes and unsubscribes for you. A payload that is a function is stored, not called.
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
// search.js
|
|
34
|
+
import { createAction } from "nanoeffects/actions";
|
|
35
|
+
|
|
36
|
+
// createAction — an event with a name; calling it builds { type, payload }
|
|
37
|
+
export const searchLoaded = createAction("search/loaded");
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
<!-- docs-check: skip - a .svelte file -->
|
|
41
|
+
```svelte
|
|
42
|
+
<script>
|
|
43
|
+
import { eventStore } from "./bus";
|
|
44
|
+
import { searchLoaded } from "./search";
|
|
45
|
+
|
|
46
|
+
// eventStore — like a store of [], but searchLoaded sets the value
|
|
47
|
+
const found = eventStore(searchLoaded, []);
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<ul>
|
|
51
|
+
{#each $found as title (title)}
|
|
52
|
+
<li>{title}</li>
|
|
53
|
+
{/each}
|
|
54
|
+
</ul>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
In TypeScript the type of the store comes from the event: with `createAction<string[]>("search/loaded")` it is `Readable<string[]>`, and for an event with no payload type it comes from `initial`.
|
|
58
|
+
|
|
59
|
+
To send a value to the bus, call `bus.emit` from the handler of the event, for example `oninput`. Nothing is emitted when the component mounts, so no plugin function is needed for that direction.
|
|
60
|
+
|
|
61
|
+
### It is not a store of the app
|
|
62
|
+
|
|
63
|
+
The bus listens only while something reads the store: the first `$found` starts it, and the last one stops it. The bus keeps no values, so an event that came while nothing read the store is lost, and a component that mounts later starts from `initial`. To keep a value for components that come later, keep it in a store of your own and write to it from a handler of the event:
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
// found-store.js
|
|
67
|
+
import { writable } from "svelte/store";
|
|
68
|
+
import { bus } from "./bus";
|
|
69
|
+
import { searchLoaded } from "./search";
|
|
70
|
+
|
|
71
|
+
// writable — a Svelte store that you keep for the whole app
|
|
72
|
+
export const foundTitles = writable([]);
|
|
73
|
+
|
|
74
|
+
// bus.on — copy every searchLoaded into the store, whether or not a component is open
|
|
75
|
+
bus.on(searchLoaded, (titles) => foundTitles.set(titles));
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Where to go next
|
|
79
|
+
|
|
80
|
+
- 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#use-a-policy-without-a-bus); Svelte runs the script once, so it needs no hook.
|
package/llms-full.txt
CHANGED
|
@@ -9,7 +9,7 @@ Rules that generated code often gets wrong:
|
|
|
9
9
|
- A handler gets the payload and a task context: `(payload, ctx)`. The context has `signal`, `emit`, `on`, `run`. Pass `ctx.signal` to `fetch`.
|
|
10
10
|
- Effects take `ctx` as the first argument: `retry(ctx, fn, options)`, `timeout(ctx, ms, fn)`, `take(ctx, "event")`.
|
|
11
11
|
- Policies wrap a handler: `bus.on("search", latest(async (query, ctx) => { ... }))`.
|
|
12
|
-
- Each part is imported from its own path: `nanoeffects` (`createBus`), `nanoeffects/policies`, `nanoeffects/effects`, `nanoeffects/actions`, `nanoeffects/redux`, `nanoeffects/observable`, `nanoeffects/react` (`reactPlugin`, adds `bus.useEmitter` and `bus.useEventState`; `usePolicy`; needs React), `nanoeffects/vue` (`vuePlugin`, adds `bus.watchEmitter` and `bus.useEventRef`; needs Vue), `nanoeffects/trace` (`createTracer`, loggers, `tracePlugin`), `nanoeffects/dev`.
|
|
12
|
+
- Each part is imported from its own path: `nanoeffects` (`createBus`), `nanoeffects/policies`, `nanoeffects/effects`, `nanoeffects/actions`, `nanoeffects/redux`, `nanoeffects/observable`, `nanoeffects/react` (`reactPlugin`, adds `bus.useEmitter` and `bus.useEventState`; `usePolicy`; needs React), `nanoeffects/svelte` (`sveltePlugin`, adds `bus.eventStore`; needs Svelte), `nanoeffects/vue` (`vuePlugin`, adds `bus.watchEmitter` and `bus.useEventRef`; needs Vue), `nanoeffects/trace` (`createTracer`, loggers, `tracePlugin`), `nanoeffects/dev`.
|
|
13
13
|
- `bus.emit(command(payload))` runs the command's handler and returns its result. Plain events return nothing.
|
|
14
14
|
|
|
15
15
|
---
|
|
@@ -553,6 +553,7 @@ Pass `--template` to skip the question. With npm, put `--` before the option.
|
|
|
553
553
|
| `react-ts` | The `react` app in TypeScript | You build a React app in TypeScript |
|
|
554
554
|
| `domains-ts` | Commands, a job and modules, with the logic split into domain folders and React as a thin layer | You build an app with more than one feature and want the logic off the UI |
|
|
555
555
|
| `vue-ts` | The `react-ts` app in Vue 3, with `watchEmitter` and `useEventRef` from `nanoeffects/vue` | You build a Vue 3 app |
|
|
556
|
+
| `svelte-ts` | The `react-ts` app in Svelte 5, with `eventStore` from `nanoeffects/svelte` | You build a Svelte app |
|
|
556
557
|
|
|
557
558
|
```bash
|
|
558
559
|
npm create nanoeffects@latest my-app -- --template react
|
|
@@ -576,6 +577,16 @@ npm create nanoeffects@latest my-app -- --template vue-ts
|
|
|
576
577
|
|
|
577
578
|
`npm run build` type-checks the `.vue` files with `vue-tsc`, which needs TypeScript 5: the template depends on it.
|
|
578
579
|
|
|
580
|
+
### Start with Svelte
|
|
581
|
+
|
|
582
|
+
`svelte-ts` is the search app of `react-ts` in Svelte 5. Its bus uses `sveltePlugin` from `nanoeffects/svelte`: `eventStore` makes a Svelte store from an event. In a component, `$found` reads it, and Svelte subscribes and unsubscribes for you. The query goes to the bus from the `oninput` handler, so nothing is emitted when the component mounts. The store listens only while something reads it, and is [not a store of the app](svelte.md#it-is-not-a-store-of-the-app): a value that several components share belongs in a store of your own.
|
|
583
|
+
|
|
584
|
+
```bash
|
|
585
|
+
npm create nanoeffects@latest my-app -- --template svelte-ts
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
`npm run build` type-checks the `.svelte` files with `svelte-check`, which needs TypeScript 5: the template depends on it.
|
|
589
|
+
|
|
579
590
|
### Start with domains
|
|
580
591
|
|
|
581
592
|
`domains-ts` is not the search box of the other templates grown in TypeScript: it shows where logic goes when an app has several features. Each domain is a folder under `src/domains/`. It has the events of the domain, the work that handles them, and a module that mounts the work. The React components only emit events and read values.
|
|
@@ -598,7 +609,7 @@ The commands, jobs and modules themselves are explained in [Actions, commands, j
|
|
|
598
609
|
|
|
599
610
|
## Read the files
|
|
600
611
|
|
|
601
|
-
Every template except `domains-ts` splits the code the way [the guide](guide.md) does: one job per file. In the TypeScript templates the files end in `.ts` (and `.tsx` for React components, `.vue` for Vue components).
|
|
612
|
+
Every template except `domains-ts` splits the code the way [the guide](guide.md) does: one job per file. In the TypeScript templates the files end in `.ts` (and `.tsx` for React components, `.vue` for Vue components, `.svelte` for Svelte components).
|
|
602
613
|
|
|
603
614
|
| File | Job |
|
|
604
615
|
| ---- | --- |
|
|
@@ -681,6 +692,7 @@ Imports are split so you only load what you use. `createBus` stays small; the re
|
|
|
681
692
|
| `nanoeffects/redux` | `createEffectsMiddleware` |
|
|
682
693
|
| `nanoeffects/observable` | `createObserve`, `Subject` |
|
|
683
694
|
| `nanoeffects/react` | `reactPlugin`: adds the `useEmitter` hook to a bus (needs React 18 or newer) |
|
|
695
|
+
| `nanoeffects/svelte` | `sveltePlugin`: adds `eventStore` to a bus (needs Svelte 5 or newer) |
|
|
684
696
|
| `nanoeffects/vue` | `vuePlugin`: adds `watchEmitter` and `useEventRef` to a bus (needs Vue 3.3 or newer) |
|
|
685
697
|
| `nanoeffects/dev` | `devPlugin` (no-op in production) |
|
|
686
698
|
| `nanoeffects/trace` | `createTracer`, the loggers and `tracePlugin` (names for the trace) |
|
|
@@ -1069,6 +1081,33 @@ Every policy accepts `key` in its options (see [Isolate state by key](#isolate-s
|
|
|
1069
1081
|
| `throttle(ms, fn, options?)` | at most one run per `ms` |
|
|
1070
1082
|
| `queue(fn, options?)` | one after another, in order |
|
|
1071
1083
|
|
|
1084
|
+
## Call a policy without its function
|
|
1085
|
+
|
|
1086
|
+
**Problem.** Some code builds the function for you and only asks which policy to use, as [`usePolicy`](react.md#keep-a-debounced-handler-between-renders) does. If you must pass your own function there, it is easy to pass the wrong one.
|
|
1087
|
+
|
|
1088
|
+
**When.** You pass a policy to `usePolicy`. With a bus, `withPolicy` or your own code, call the policy with its function as above.
|
|
1089
|
+
|
|
1090
|
+
Leave the function out and the policy returns a wrapper. The wrapper takes a function and gives back the same policy as the direct call:
|
|
1091
|
+
|
|
1092
|
+
```js
|
|
1093
|
+
import { debounce } from "nanoeffects/policies";
|
|
1094
|
+
|
|
1095
|
+
const runSearch = async (query) => {
|
|
1096
|
+
await api.search(query);
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
// debounce(300) — no function yet: returns a wrapper
|
|
1100
|
+
const wrap = debounce(300);
|
|
1101
|
+
|
|
1102
|
+
// the wrapper takes the function; same as debounce(300, runSearch)
|
|
1103
|
+
const search = wrap(runSearch);
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1106
|
+
- It works for `latest()`, `leading()`, `debounce(ms)`, `throttle(ms)` and `queue()`, with the options as the last argument: `queue({ concurrency: 2 })`, `debounce(300, { leading: true })`.
|
|
1107
|
+
- Each time you apply a wrapper to a function, you get a new policy with its own state.
|
|
1108
|
+
- To combine policies, nest them as before: `throttle(100, latest(fn))`.
|
|
1109
|
+
- With `key`, annotate the payload of the key function in TypeScript: `latest({ key: (user: User) => user.id })`. Without the annotation the wrapper cannot know the payload type.
|
|
1110
|
+
|
|
1072
1111
|
## Configure debounce and throttle
|
|
1073
1112
|
|
|
1074
1113
|
`debounce` and `throttle` take `leading` and `trailing`. Behavior matches lodash (and patronum); tests compare random call sequences to lodash itself.
|
|
@@ -1789,18 +1828,24 @@ bus.on("watch", (_, ctx) => {
|
|
|
1789
1828
|
- Under `latest`, subscriptions of a superseded run end with that run.
|
|
1790
1829
|
- `ctx.off(event)` clears every handler of the event on the bus, not only the task’s.
|
|
1791
1830
|
|
|
1792
|
-
`take(ctx, event, options?)` waits for the next `event` and resolves with its payload
|
|
1831
|
+
`take(ctx, event, options?)` waits for the next `event` and resolves with its payload. `event` is the name of an event or an action creator, as for `bus.on`:
|
|
1793
1832
|
|
|
1794
1833
|
```js
|
|
1795
1834
|
import { createBus } from "nanoeffects";
|
|
1835
|
+
import { createAction } from "nanoeffects/actions";
|
|
1796
1836
|
import { take, timeout } from "nanoeffects/effects";
|
|
1797
1837
|
|
|
1798
1838
|
const bus = createBus();
|
|
1839
|
+
const stopRequested = createAction("stop"); // an event with a name; calling it builds the action
|
|
1799
1840
|
|
|
1800
1841
|
bus.on("login", async (user, ctx) => {
|
|
1801
1842
|
const reason = await take(ctx, "logout"); // resolves on the next logout
|
|
1802
1843
|
cleanup(user, reason);
|
|
1803
1844
|
|
|
1845
|
+
// an action creator works the same, and its payload type is the type of the result
|
|
1846
|
+
const stopped = await take(ctx, stopRequested);
|
|
1847
|
+
console.log(stopped);
|
|
1848
|
+
|
|
1804
1849
|
// wait for a matching event, but not longer than 5 s
|
|
1805
1850
|
const id = await timeout(ctx, 5000, ({ signal }) =>
|
|
1806
1851
|
take(ctx, "saved", { signal, predicate: (savedId) => savedId === draft.id }),
|
|
@@ -1809,6 +1854,7 @@ bus.on("login", async (user, ctx) => {
|
|
|
1809
1854
|
```
|
|
1810
1855
|
|
|
1811
1856
|
- Sees only events emitted after the call, takes one, and unsubscribes.
|
|
1857
|
+
- With an action creator the result has the payload type of the creator. A creator made without a payload type (`createAction("saved")`) takes it from the event map of the bus.
|
|
1812
1858
|
- `options.predicate` skips payloads it rejects. In TypeScript, a type guard (`(payload): payload is Admin`) narrows the type of the result.
|
|
1813
1859
|
- `options.signal` stops the wait (how a `race` or `timeout` starter cancels it).
|
|
1814
1860
|
- On abort of the task or of `options.signal`, unsubscribes and rejects with the abort reason.
|
|
@@ -2249,7 +2295,7 @@ Redux’s `dispatch` → reducers updates **state**. It does not give you first-
|
|
|
2249
2295
|
|
|
2250
2296
|
`createEffectsMiddleware(bus)` does the same job: after the reducer runs, the action also hits the nanoeffects bus you pass to it. Keep RTK for state; use policies and effects for async.
|
|
2251
2297
|
|
|
2252
|
-
Skip this section if you are not already on Redux. The recipe as a file, type-checked with the project: [`examples/redux.ts`](../examples/redux.ts).
|
|
2298
|
+
Skip this section if you are not already on Redux. The recipe as a file, type-checked with the project: [`examples/redux.ts`](../examples/redux.ts). To run it, open the [counter example](../examples/counter): `redux.html` is a counter with Redux Toolkit and this middleware.
|
|
2253
2299
|
|
|
2254
2300
|
```js
|
|
2255
2301
|
// bus.js
|
|
@@ -3022,6 +3068,8 @@ Send it from the handler with `ctx.emit(userLoaded(user))`. Do not replace the c
|
|
|
3022
3068
|
- **Give each request its own signal for an event.** For a command or a job the link ends when it settles. For `bus.emit(action, { signal })` it stays until the signal aborts.
|
|
3023
3069
|
- **The signal reaches the tasks that this `emit` starts**, with the listeners of the command's type and the `ctx.run` children. A separate `bus.emit` that the handler makes is not linked: pass `{ signal: ctx.signal }` to it.
|
|
3024
3070
|
|
|
3071
|
+
A whole small app, with a search, a filter, pages and a detail panel, is in the [Rick and Morty example](../examples/rickandmorty).
|
|
3072
|
+
|
|
3025
3073
|
---
|
|
3026
3074
|
Source: docs/react.md
|
|
3027
3075
|
|
|
@@ -3275,8 +3323,8 @@ export const Search = ({ wait }) => {
|
|
|
3275
3323
|
const found = await api.search(query, { signal });
|
|
3276
3324
|
setItems(found);
|
|
3277
3325
|
},
|
|
3278
|
-
// the second argument builds the policy once
|
|
3279
|
-
|
|
3326
|
+
// debounce(wait) — the second argument builds the policy once around the callback above
|
|
3327
|
+
debounce(wait),
|
|
3280
3328
|
// deps — list what the second argument reads; a new wait makes a new policy
|
|
3281
3329
|
[wait],
|
|
3282
3330
|
);
|
|
@@ -3292,7 +3340,9 @@ export const Search = ({ wait }) => {
|
|
|
3292
3340
|
|
|
3293
3341
|
- `onChange` keeps its identity while `wait` stays the same, so it is safe in `useEffect` and in props.
|
|
3294
3342
|
- A pending call is cancelled on unmount and when `deps` change.
|
|
3295
|
-
-
|
|
3343
|
+
- Write the policy without its callback: `debounce(300)`, not `debounce(300, callback)`. `usePolicy` puts the callback in itself, always the one of the latest render.
|
|
3344
|
+
- To combine policies, write a function of the task: `(task) => throttle(100, latest(task))`. Here `task` is the callback that `usePolicy` keeps current; pass it, not your own function.
|
|
3345
|
+
- `deps` stay your job: `debounce(wait)` is made again on every render, but only the first one (and the one after `deps` change) is used.
|
|
3296
3346
|
- Without a React component, use [`withPolicy`](policies.md#use-a-policy-without-a-bus).
|
|
3297
3347
|
|
|
3298
3348
|
## Where to go next
|
|
@@ -3352,7 +3402,7 @@ Each domain is a folder. The components only emit events and read values; they n
|
|
|
3352
3402
|
| `src/domains/incoming/` | New messages, asked for at an interval. |
|
|
3353
3403
|
| `src/ui/` | The components. |
|
|
3354
3404
|
|
|
3355
|
-
A domain folder has its events (`rooms.ts`, which imports no bus), its work (`rooms-handlers.ts`), the state it keeps (`rooms-store.ts`) and a module (`rooms-module.ts`) that wires them. A domain reacts to another domain by listening to its events, never by importing its handlers or its store.
|
|
3405
|
+
A domain folder has its events (`rooms.ts`, which imports no bus), its work (`rooms-handlers.ts`), the state it keeps (`rooms-store.ts`) and a module (`rooms-module.ts`) that wires them. A domain reacts to another domain by listening to its events, never by importing its handlers or its store. Events and commands both live in the events file. An event (`createAction`) says that something happened, and any domain may listen: `rooms/selected` is heard by three. A command (`createCommand`) is an order with exactly one handler, as `search/openResult` and `messages/mark` are. The handler is given to `createCommand`, so the command works wherever it is called and can return a result, and the module binds it with `module.use` for as long as it is mounted. A job is a command that also sends its outcomes, but its handler is bound in the module (`module.handle`): a job usually runs under a policy, and the type of its outcomes is exact only when the handler is not part of the type.
|
|
3356
3406
|
|
|
3357
3407
|
## Load a history that must not race
|
|
3358
3408
|
|
|
@@ -3481,19 +3531,24 @@ export const runSearch = debounce(
|
|
|
3481
3531
|
`take` waits for the next event of a name. Start it before the emit that causes the event, so nothing is missed:
|
|
3482
3532
|
|
|
3483
3533
|
```js
|
|
3534
|
+
import { createCommand } from "nanoeffects/actions";
|
|
3484
3535
|
import { take } from "nanoeffects/effects";
|
|
3485
3536
|
|
|
3537
|
+
// createCommand — an order for the messages domain, with the handler that does it
|
|
3538
|
+
const markMessage = createCommand("messages/mark", (messageId) => console.log("mark", messageId));
|
|
3539
|
+
|
|
3486
3540
|
// the room that is open, kept from the rooms/selected event
|
|
3487
3541
|
let selectedRoom = "general";
|
|
3488
3542
|
|
|
3489
|
-
export const
|
|
3543
|
+
export const goToResult = async (hit, ctx) => {
|
|
3490
3544
|
if (hit.roomId !== selectedRoom) {
|
|
3491
3545
|
// take — wait for the next event of that name; started before the emit so nothing is missed
|
|
3492
3546
|
const loaded = take(ctx, "messages/loadHistory/success");
|
|
3493
3547
|
ctx.emit("rooms/selected", hit.roomId);
|
|
3494
3548
|
await loaded;
|
|
3495
3549
|
}
|
|
3496
|
-
ctx.emit
|
|
3550
|
+
// ctx.emit with a command — the messages domain marks the message
|
|
3551
|
+
ctx.emit(markMessage(hit.id));
|
|
3497
3552
|
};
|
|
3498
3553
|
```
|
|
3499
3554
|
|
|
@@ -3648,6 +3703,91 @@ In TypeScript the type of the `ref` comes from the event: with `createAction<str
|
|
|
3648
3703
|
- A policy in a component (a debounced save): in `<script setup>` make it once with [`withPolicy`](policies.md#use-a-policy-without-a-bus); Vue runs the setup once, so it needs no hook.
|
|
3649
3704
|
- Keep values in Pinia: [Connect Redux, Zustand, MobX, or Pinia](stores.md).
|
|
3650
3705
|
|
|
3706
|
+
---
|
|
3707
|
+
Source: docs/svelte.md
|
|
3708
|
+
|
|
3709
|
+
# Use nanoeffects in a Svelte app
|
|
3710
|
+
|
|
3711
|
+
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).
|
|
3712
|
+
|
|
3713
|
+
You need Svelte 5 or newer and `nanoeffects` installed. If you do not know what the bus is, read the [Guide](guide.md) first.
|
|
3714
|
+
|
|
3715
|
+
## Add the plugin to the bus
|
|
3716
|
+
|
|
3717
|
+
**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.
|
|
3718
|
+
|
|
3719
|
+
**When.** A component shows the payload of an event, such as the result of a search.
|
|
3720
|
+
|
|
3721
|
+
`sveltePlugin()` adds `eventStore` to the bus. Take it out of the bus once, in the file that makes it, and import it where you need it:
|
|
3722
|
+
|
|
3723
|
+
```js
|
|
3724
|
+
// bus.js
|
|
3725
|
+
import { createBus } from "nanoeffects";
|
|
3726
|
+
import { sveltePlugin } from "nanoeffects/svelte";
|
|
3727
|
+
|
|
3728
|
+
// createBus — the event bus, created once outside Svelte
|
|
3729
|
+
// sveltePlugin — adds eventStore to the bus
|
|
3730
|
+
export const bus = createBus().use(sveltePlugin());
|
|
3731
|
+
export const { eventStore } = bus; // a free function: it does not read `this`
|
|
3732
|
+
```
|
|
3733
|
+
|
|
3734
|
+
## Show the result of an event
|
|
3735
|
+
|
|
3736
|
+
<!-- docs-check: continue -->
|
|
3737
|
+
|
|
3738
|
+
`eventStore(event, initial)` returns a Svelte store. Its value is `initial` at first and the payload of every `event` after that. In a component, `$found` reads it, and Svelte subscribes and unsubscribes for you. A payload that is a function is stored, not called.
|
|
3739
|
+
|
|
3740
|
+
```js
|
|
3741
|
+
// search.js
|
|
3742
|
+
import { createAction } from "nanoeffects/actions";
|
|
3743
|
+
|
|
3744
|
+
// createAction — an event with a name; calling it builds { type, payload }
|
|
3745
|
+
export const searchLoaded = createAction("search/loaded");
|
|
3746
|
+
```
|
|
3747
|
+
|
|
3748
|
+
<!-- docs-check: skip - a .svelte file -->
|
|
3749
|
+
```svelte
|
|
3750
|
+
<script>
|
|
3751
|
+
import { eventStore } from "./bus";
|
|
3752
|
+
import { searchLoaded } from "./search";
|
|
3753
|
+
|
|
3754
|
+
// eventStore — like a store of [], but searchLoaded sets the value
|
|
3755
|
+
const found = eventStore(searchLoaded, []);
|
|
3756
|
+
</script>
|
|
3757
|
+
|
|
3758
|
+
<ul>
|
|
3759
|
+
{#each $found as title (title)}
|
|
3760
|
+
<li>{title}</li>
|
|
3761
|
+
{/each}
|
|
3762
|
+
</ul>
|
|
3763
|
+
```
|
|
3764
|
+
|
|
3765
|
+
In TypeScript the type of the store comes from the event: with `createAction<string[]>("search/loaded")` it is `Readable<string[]>`, and for an event with no payload type it comes from `initial`.
|
|
3766
|
+
|
|
3767
|
+
To send a value to the bus, call `bus.emit` from the handler of the event, for example `oninput`. Nothing is emitted when the component mounts, so no plugin function is needed for that direction.
|
|
3768
|
+
|
|
3769
|
+
### It is not a store of the app
|
|
3770
|
+
|
|
3771
|
+
The bus listens only while something reads the store: the first `$found` starts it, and the last one stops it. The bus keeps no values, so an event that came while nothing read the store is lost, and a component that mounts later starts from `initial`. To keep a value for components that come later, keep it in a store of your own and write to it from a handler of the event:
|
|
3772
|
+
|
|
3773
|
+
```js
|
|
3774
|
+
// found-store.js
|
|
3775
|
+
import { writable } from "svelte/store";
|
|
3776
|
+
import { bus } from "./bus";
|
|
3777
|
+
import { searchLoaded } from "./search";
|
|
3778
|
+
|
|
3779
|
+
// writable — a Svelte store that you keep for the whole app
|
|
3780
|
+
export const foundTitles = writable([]);
|
|
3781
|
+
|
|
3782
|
+
// bus.on — copy every searchLoaded into the store, whether or not a component is open
|
|
3783
|
+
bus.on(searchLoaded, (titles) => foundTitles.set(titles));
|
|
3784
|
+
```
|
|
3785
|
+
|
|
3786
|
+
## Where to go next
|
|
3787
|
+
|
|
3788
|
+
- The same job for React: [Build a sign-up form in React](react.md); for Vue: [Use nanoeffects in a Vue app](vue.md).
|
|
3789
|
+
- A policy in a component (a debounced save): in the script of a component make it once with [`withPolicy`](policies.md#use-a-policy-without-a-bus); Svelte runs the script once, so it needs no hook.
|
|
3790
|
+
|
|
3651
3791
|
---
|
|
3652
3792
|
Source: docs/context.md
|
|
3653
3793
|
|
|
@@ -4586,7 +4726,7 @@ Read this page if you know redux-saga and want to find the same job in nanoeffec
|
|
|
4586
4726
|
| `fork(fn, x)` | `ctx.run(task, x)` without `await` |
|
|
4587
4727
|
| `spawn(fn, x)` | `ctx.emit(event, x)` or `bus.emit(event, x)` |
|
|
4588
4728
|
| `put(action)` | `ctx.emit(action)` |
|
|
4589
|
-
| `take(pattern)` | [`take(ctx, event, { predicate })`](effects.md) |
|
|
4729
|
+
| `take(pattern)` | [`take(ctx, event, { predicate })`](effects.md), where `event` is a name or an action creator |
|
|
4590
4730
|
| `select` | a [context field](context.md), such as `store` |
|
|
4591
4731
|
| `all`, `race` | [`all`, `race`](effects.md) |
|
|
4592
4732
|
| `retry`, `delay` | [`retry`, `delay`](effects.md) |
|
|
@@ -4634,6 +4774,8 @@ bus.on(
|
|
|
4634
4774
|
|
|
4635
4775
|
Tasks are plain `async` functions. `ctx.signal` replaces `cancel`: pass it to `fetch` or to your client, and the request stops.
|
|
4636
4776
|
|
|
4777
|
+
To see the counter, `takeLatest` with `delay`, a cancel and a failing call running, open the [counter example](../examples/counter): it has a page without a store and a page with Redux Toolkit.
|
|
4778
|
+
|
|
4637
4779
|
## Start a task without waiting
|
|
4638
4780
|
|
|
4639
4781
|
`fork` and `spawn` differ in what happens when the caller is aborted. Pick by that.
|
package/llms.txt
CHANGED
|
@@ -9,13 +9,13 @@ Rules that generated code often gets wrong:
|
|
|
9
9
|
- A handler gets the payload and a task context: `(payload, ctx)`. The context has `signal`, `emit`, `on`, `run`. Pass `ctx.signal` to `fetch`.
|
|
10
10
|
- Effects take `ctx` as the first argument: `retry(ctx, fn, options)`, `timeout(ctx, ms, fn)`, `take(ctx, "event")`.
|
|
11
11
|
- Policies wrap a handler: `bus.on("search", latest(async (query, ctx) => { ... }))`.
|
|
12
|
-
- Each part is imported from its own path: `nanoeffects` (`createBus`), `nanoeffects/policies`, `nanoeffects/effects`, `nanoeffects/actions`, `nanoeffects/redux`, `nanoeffects/observable`, `nanoeffects/react` (`reactPlugin`, adds `bus.useEmitter` and `bus.useEventState`; `usePolicy`; needs React), `nanoeffects/vue` (`vuePlugin`, adds `bus.watchEmitter` and `bus.useEventRef`; needs Vue), `nanoeffects/trace` (`createTracer`, loggers, `tracePlugin`), `nanoeffects/dev`.
|
|
12
|
+
- Each part is imported from its own path: `nanoeffects` (`createBus`), `nanoeffects/policies`, `nanoeffects/effects`, `nanoeffects/actions`, `nanoeffects/redux`, `nanoeffects/observable`, `nanoeffects/react` (`reactPlugin`, adds `bus.useEmitter` and `bus.useEventState`; `usePolicy`; needs React), `nanoeffects/svelte` (`sveltePlugin`, adds `bus.eventStore`; needs Svelte), `nanoeffects/vue` (`vuePlugin`, adds `bus.watchEmitter` and `bus.useEventRef`; needs Vue), `nanoeffects/trace` (`createTracer`, loggers, `tracePlugin`), `nanoeffects/dev`.
|
|
13
13
|
- `bus.emit(command(payload))` runs the command's handler and returns its result. Plain events return nothing.
|
|
14
14
|
|
|
15
15
|
## Start here
|
|
16
16
|
|
|
17
17
|
- [Guide](docs/guide.md): bus, handler context, policies, effects, commands and modules, step by step
|
|
18
|
-
- [Start a project from a template](docs/starter.md): `npm create nanoeffects` makes a Vite project (`vanilla`, `react`, `vue-ts` and others, in JavaScript or TypeScript) with a bus, `debounce` and `latest`
|
|
18
|
+
- [Start a project from a template](docs/starter.md): `npm create nanoeffects` makes a Vite project (`vanilla`, `react`, `vue-ts`, `svelte-ts` and others, in JavaScript or TypeScript) with a bus, `debounce` and `latest`
|
|
19
19
|
- [Why nanoeffects exists](docs/motivation.md): no dependencies and no global state for SDKs and Module Federation, and why not Redux, Effector or RxJS
|
|
20
20
|
- [Package entries](docs/package.md): which import holds what
|
|
21
21
|
- [Plugins](docs/plugins.md): every plugin of a bus, what `bus.use` returns, and the order to use them in
|
|
@@ -37,6 +37,7 @@ Rules that generated code often gets wrong:
|
|
|
37
37
|
- [Build a sign-up form in React](docs/react.md): a form with a username check, a submit that cannot fire twice, and retry; `useEventState` shows an event as state, `usePolicy` keeps a debounced handler between renders
|
|
38
38
|
- [Build a chat with domains, modules and a small server](docs/chat-example.md): rooms, a history under `latest`, a send under `queue` and `retry`, a search with `allSettled` and `timeout`, new messages polled with `poll`
|
|
39
39
|
- [Use nanoeffects in a Vue app](docs/vue.md): `vuePlugin` adds `watchEmitter` (a value of a component to the bus) and `useEventRef` (an event as a ref)
|
|
40
|
+
- [Use nanoeffects in a Svelte app](docs/svelte.md): `sveltePlugin` adds `eventStore` (an event as a Svelte store)
|
|
40
41
|
- [Context fields](docs/context.md): put a store or client on every task
|
|
41
42
|
- [Scopes](docs/scope.md): fields for one command, action, job, module or `emit`, and a way to replace them in a test
|
|
42
43
|
- [Use the bus from a class](docs/oop.md): `Emitter`, `extendBus`, `@WithBus`, `@On`, `@Once`, and `dispose()` for the subscriptions of an instance
|