nanoeffects 0.18.0 → 0.19.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/docs/actions.md +2 -0
- package/docs/react.md +2 -0
- package/docs/starter.md +22 -1
- package/docs/stores.md +12 -12
- package/llms-full.txt +38 -13
- package/package.json +1 -1
package/docs/actions.md
CHANGED
|
@@ -6,6 +6,8 @@ If you only need `on` / `emit` or policies, stay in the [Guide](guide.md) until
|
|
|
6
6
|
|
|
7
7
|
Recipe without a UI framework: [`examples/modules.ts`](../examples/modules.ts).
|
|
8
8
|
|
|
9
|
+
A whole project with two domains, a job and React as a thin layer: `npm create nanoeffects@latest my-app -- --template domains-ts` (see [Start a project from a template](starter.md#start-with-domains)).
|
|
10
|
+
|
|
9
11
|
## Return a value from `emit` with a command
|
|
10
12
|
|
|
11
13
|
The bus lives in one file and its events in another, so any module can import both without a cycle.
|
package/docs/react.md
CHANGED
|
@@ -220,6 +220,8 @@ export function Found() {
|
|
|
220
220
|
|
|
221
221
|
`setFound` is the setter of React. It changes the state of this component only and emits nothing.
|
|
222
222
|
|
|
223
|
+
`useEventState` is not a store. Each component that calls it keeps its own copy of the last payload, and a component that mounts later does not see earlier events. To share a value between components, or to keep it for components that mount later, keep it in a state manager (Redux, Zustand, Jotai or another) and write to it from a handler of the event. See [Stores](stores.md). Without a state manager, one `useEventState` in a Context provider at the root shares the value with every component below it: [Plain React: Context, `useEmitter` and `useEventState`](stores.md#plain-react-context-useemitter-and-useeventstate).
|
|
224
|
+
|
|
223
225
|
In TypeScript, the state type comes from the event: from `createAction<string[]>("search/loaded")` or from the event map of the bus. When the event has no payload type, as in the example above, it comes from `initial`: `["a"]` is `string[]`, and an empty `[]` is `unknown[]`. Give the event a type, or pass a typed `initial`, when you need more.
|
|
224
226
|
|
|
225
227
|
The bus keeps no values, so the hook cannot replay an event. An event emitted before the component mounts is not seen: if that matters, read the current value into `initial`.
|
package/docs/starter.md
CHANGED
|
@@ -46,20 +46,41 @@ Pass `--template` to skip the question. With npm, put `--` before the option.
|
|
|
46
46
|
| `react` | The same handler with `useEmitter` and `useEventState`, and `usePolicy` for a debounced save in a component | You build a React app |
|
|
47
47
|
| `vanilla-ts` | The `vanilla` app in TypeScript | You want the compiler to check events and handlers |
|
|
48
48
|
| `react-ts` | The `react` app in TypeScript | You build a React app in TypeScript |
|
|
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 |
|
|
49
50
|
|
|
50
51
|
```bash
|
|
51
52
|
npm create nanoeffects@latest my-app -- --template react
|
|
52
53
|
```
|
|
53
54
|
|
|
55
|
+
In the `react` templates, `useEventState` keeps the found items in the component. It is not a global store: another component gets its own copy. To share a value between components, use a state manager and see [Stores](stores.md).
|
|
56
|
+
|
|
54
57
|
The TypeScript templates are the same apps with the same files, written in `.ts` and `.tsx`. Events are made with `createAction<Payload>`, so handlers and `useEventState` get their types from the events, with no annotation in the component. `npm run build` type-checks the project (`tsc --noEmit`) before bundling it.
|
|
55
58
|
|
|
56
59
|
```bash
|
|
57
60
|
npm create nanoeffects@latest my-app -- --template react-ts
|
|
58
61
|
```
|
|
59
62
|
|
|
63
|
+
### Start with domains
|
|
64
|
+
|
|
65
|
+
`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.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm create nanoeffects@latest my-app -- --template domains-ts
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
| Path | Job |
|
|
72
|
+
| ---- | --- |
|
|
73
|
+
| `src/bus.ts` | Creates the bus. It imports no events and no domain. |
|
|
74
|
+
| `src/app.ts` | Mounts the domains with `modulePlugin`. |
|
|
75
|
+
| `src/domains/catalog/` | Searching products: `catalog.ts` (events and a command), `catalog-handlers.ts` (`debounce` and `latest`), `catalog-module.ts`. |
|
|
76
|
+
| `src/domains/checkout/` | Placing an order: `checkout.ts` (a job), `checkout-handlers.ts` (`leading` and `retry`), `checkout-store.ts` (state written from the job's outcomes), `checkout-module.ts`. |
|
|
77
|
+
| `src/ui/` | The components. None of them calls the fake server in `src/api.ts`. |
|
|
78
|
+
|
|
79
|
+
The commands, jobs and modules themselves are explained in [Actions, commands, jobs and modules](actions.md). The template keeps found products in a Context provider with one `useEventState`, and the order state in a store of its own: `useEventState` is [not a store](react.md#show-the-last-value-of-an-event-as-state).
|
|
80
|
+
|
|
60
81
|
## Read the files
|
|
61
82
|
|
|
62
|
-
Every template 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 components).
|
|
83
|
+
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 components).
|
|
63
84
|
|
|
64
85
|
| File | Job |
|
|
65
86
|
| ---- | --- |
|
package/docs/stores.md
CHANGED
|
@@ -365,23 +365,23 @@ A value of your UI (a query, a filter, a name) often has two jobs. Several compo
|
|
|
365
365
|
|
|
366
366
|
| The value lives in | Use when |
|
|
367
367
|
| --- | --- |
|
|
368
|
-
| A Context provider with `useState` | Only components read it. Plain React, one `useEmitter` call. |
|
|
368
|
+
| A Context provider with `useState` and `useEventState` | Only components read it. Plain React, one `useEmitter` and one `useEventState` call. |
|
|
369
369
|
| A small store on `useSyncExternalStore` | A bus handler, a test or a timer must read it too. |
|
|
370
370
|
| An atom of a store library | You already use Nanostores, Jotai and the like. |
|
|
371
371
|
|
|
372
|
-
#### Plain React: Context and `
|
|
372
|
+
#### Plain React: Context, `useEmitter` and `useEventState`
|
|
373
373
|
|
|
374
374
|
Writing `useEffect(() => bus.emit(...), [value])` looks enough, but it emits on mount, emits twice on mount under StrictMode, and re-runs when the emit function is new on every render. `useEmitter` from `nanoeffects/react` avoids all three. It emits after a render commits with a changed value, never on mount, and compares with `Object.is`. You need React 18 or newer; no other entry of the package uses React.
|
|
375
375
|
|
|
376
|
-
First the bus, the events and the handlers, each in its own file. `reactPlugin()` adds `bus.useEmitter`, and destructuring
|
|
376
|
+
First the bus, the events and the handlers, each in its own file. `reactPlugin()` adds `bus.useEmitter` and `bus.useEventState`, and destructuring them gives plain hooks that you export from the bus file:
|
|
377
377
|
|
|
378
378
|
```js
|
|
379
379
|
// bus.js
|
|
380
380
|
import { createBus } from "nanoeffects";
|
|
381
381
|
import { reactPlugin } from "nanoeffects/react";
|
|
382
382
|
|
|
383
|
-
export const bus = createBus().use(reactPlugin()); // the bus; reactPlugin adds
|
|
384
|
-
export const { useEmitter } = bus; //
|
|
383
|
+
export const bus = createBus().use(reactPlugin()); // the bus; reactPlugin adds the React hooks
|
|
384
|
+
export const { useEmitter, useEventState } = bus; // free hooks: they do not read `this`
|
|
385
385
|
```
|
|
386
386
|
|
|
387
387
|
```js
|
|
@@ -411,25 +411,24 @@ bus.on(
|
|
|
411
411
|
);
|
|
412
412
|
```
|
|
413
413
|
|
|
414
|
-
Then the provider. It holds the state, calls `useEmitter` once, and
|
|
414
|
+
Then the provider. It holds the state, calls `useEmitter` once to send the query to the bus, and `useEventState` once to receive the result. Every component below it only reads the context:
|
|
415
415
|
|
|
416
416
|
```jsx
|
|
417
417
|
// SearchProvider.jsx
|
|
418
|
-
import { createContext, useContext,
|
|
419
|
-
import {
|
|
418
|
+
import { createContext, useContext, useState } from "react";
|
|
419
|
+
import { useEmitter, useEventState } from "./bus";
|
|
420
420
|
import { queryChanged, searchLoaded } from "./search";
|
|
421
421
|
|
|
422
422
|
const SearchContext = createContext(null);
|
|
423
423
|
|
|
424
424
|
export function SearchProvider({ children }) {
|
|
425
425
|
const [query, setQuery] = useState("");
|
|
426
|
-
const [items, setItems] = useState([]);
|
|
427
426
|
|
|
428
427
|
// React → bus: emits queryChanged(query) after every change of query, not on mount
|
|
429
428
|
useEmitter(queryChanged, query);
|
|
430
429
|
|
|
431
|
-
// bus → React:
|
|
432
|
-
|
|
430
|
+
// bus → React: like useState([]), but searchLoaded sets the value; one subscription for the whole app
|
|
431
|
+
const [items] = useEventState(searchLoaded, []);
|
|
433
432
|
|
|
434
433
|
return (
|
|
435
434
|
<SearchContext.Provider value={{ query, setQuery, items }}>
|
|
@@ -463,8 +462,9 @@ export function Results() {
|
|
|
463
462
|
}
|
|
464
463
|
```
|
|
465
464
|
|
|
466
|
-
|
|
465
|
+
Three limits to know:
|
|
467
466
|
|
|
467
|
+
- **The provider must be mounted first.** `useEventState` is not a store: the bus keeps no values, so a result that arrives before the provider mounts, or after it unmounts, is lost. Put the provider at the root. If a value must survive that, keep it in a store below and write to it from a handler.
|
|
468
468
|
- **All consumers re-render** on any change of the context value. Split it into two contexts (`query` and `items`) if that is slow.
|
|
469
469
|
- **A new object counts as a change.** For a value that is rebuilt with the same content, pass `equals` in the options, the third argument, to say when two values are the same: `useEmitter(profileChanged, profile, { equals: (previous, next) => previous.id === next.id })`. The same object takes `traceId` and `traceGroupId`, which name the subscription and the flow in the [trace](tracing.md#show-which-subscription-began-a-chain).
|
|
470
470
|
|
package/llms-full.txt
CHANGED
|
@@ -551,20 +551,41 @@ Pass `--template` to skip the question. With npm, put `--` before the option.
|
|
|
551
551
|
| `react` | The same handler with `useEmitter` and `useEventState`, and `usePolicy` for a debounced save in a component | You build a React app |
|
|
552
552
|
| `vanilla-ts` | The `vanilla` app in TypeScript | You want the compiler to check events and handlers |
|
|
553
553
|
| `react-ts` | The `react` app in TypeScript | You build a React app in TypeScript |
|
|
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 |
|
|
554
555
|
|
|
555
556
|
```bash
|
|
556
557
|
npm create nanoeffects@latest my-app -- --template react
|
|
557
558
|
```
|
|
558
559
|
|
|
560
|
+
In the `react` templates, `useEventState` keeps the found items in the component. It is not a global store: another component gets its own copy. To share a value between components, use a state manager and see [Stores](stores.md).
|
|
561
|
+
|
|
559
562
|
The TypeScript templates are the same apps with the same files, written in `.ts` and `.tsx`. Events are made with `createAction<Payload>`, so handlers and `useEventState` get their types from the events, with no annotation in the component. `npm run build` type-checks the project (`tsc --noEmit`) before bundling it.
|
|
560
563
|
|
|
561
564
|
```bash
|
|
562
565
|
npm create nanoeffects@latest my-app -- --template react-ts
|
|
563
566
|
```
|
|
564
567
|
|
|
568
|
+
### Start with domains
|
|
569
|
+
|
|
570
|
+
`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.
|
|
571
|
+
|
|
572
|
+
```bash
|
|
573
|
+
npm create nanoeffects@latest my-app -- --template domains-ts
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
| Path | Job |
|
|
577
|
+
| ---- | --- |
|
|
578
|
+
| `src/bus.ts` | Creates the bus. It imports no events and no domain. |
|
|
579
|
+
| `src/app.ts` | Mounts the domains with `modulePlugin`. |
|
|
580
|
+
| `src/domains/catalog/` | Searching products: `catalog.ts` (events and a command), `catalog-handlers.ts` (`debounce` and `latest`), `catalog-module.ts`. |
|
|
581
|
+
| `src/domains/checkout/` | Placing an order: `checkout.ts` (a job), `checkout-handlers.ts` (`leading` and `retry`), `checkout-store.ts` (state written from the job's outcomes), `checkout-module.ts`. |
|
|
582
|
+
| `src/ui/` | The components. None of them calls the fake server in `src/api.ts`. |
|
|
583
|
+
|
|
584
|
+
The commands, jobs and modules themselves are explained in [Actions, commands, jobs and modules](actions.md). The template keeps found products in a Context provider with one `useEventState`, and the order state in a store of its own: `useEventState` is [not a store](react.md#show-the-last-value-of-an-event-as-state).
|
|
585
|
+
|
|
565
586
|
## Read the files
|
|
566
587
|
|
|
567
|
-
Every template 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 components).
|
|
588
|
+
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 components).
|
|
568
589
|
|
|
569
590
|
| File | Job |
|
|
570
591
|
| ---- | --- |
|
|
@@ -1861,6 +1882,8 @@ If you only need `on` / `emit` or policies, stay in the [Guide](guide.md) until
|
|
|
1861
1882
|
|
|
1862
1883
|
Recipe without a UI framework: [`examples/modules.ts`](../examples/modules.ts).
|
|
1863
1884
|
|
|
1885
|
+
A whole project with two domains, a job and React as a thin layer: `npm create nanoeffects@latest my-app -- --template domains-ts` (see [Start a project from a template](starter.md#start-with-domains)).
|
|
1886
|
+
|
|
1864
1887
|
## Return a value from `emit` with a command
|
|
1865
1888
|
|
|
1866
1889
|
The bus lives in one file and its events in another, so any module can import both without a cycle.
|
|
@@ -2522,23 +2545,23 @@ A value of your UI (a query, a filter, a name) often has two jobs. Several compo
|
|
|
2522
2545
|
|
|
2523
2546
|
| The value lives in | Use when |
|
|
2524
2547
|
| --- | --- |
|
|
2525
|
-
| A Context provider with `useState` | Only components read it. Plain React, one `useEmitter` call. |
|
|
2548
|
+
| A Context provider with `useState` and `useEventState` | Only components read it. Plain React, one `useEmitter` and one `useEventState` call. |
|
|
2526
2549
|
| A small store on `useSyncExternalStore` | A bus handler, a test or a timer must read it too. |
|
|
2527
2550
|
| An atom of a store library | You already use Nanostores, Jotai and the like. |
|
|
2528
2551
|
|
|
2529
|
-
#### Plain React: Context and `
|
|
2552
|
+
#### Plain React: Context, `useEmitter` and `useEventState`
|
|
2530
2553
|
|
|
2531
2554
|
Writing `useEffect(() => bus.emit(...), [value])` looks enough, but it emits on mount, emits twice on mount under StrictMode, and re-runs when the emit function is new on every render. `useEmitter` from `nanoeffects/react` avoids all three. It emits after a render commits with a changed value, never on mount, and compares with `Object.is`. You need React 18 or newer; no other entry of the package uses React.
|
|
2532
2555
|
|
|
2533
|
-
First the bus, the events and the handlers, each in its own file. `reactPlugin()` adds `bus.useEmitter`, and destructuring
|
|
2556
|
+
First the bus, the events and the handlers, each in its own file. `reactPlugin()` adds `bus.useEmitter` and `bus.useEventState`, and destructuring them gives plain hooks that you export from the bus file:
|
|
2534
2557
|
|
|
2535
2558
|
```js
|
|
2536
2559
|
// bus.js
|
|
2537
2560
|
import { createBus } from "nanoeffects";
|
|
2538
2561
|
import { reactPlugin } from "nanoeffects/react";
|
|
2539
2562
|
|
|
2540
|
-
export const bus = createBus().use(reactPlugin()); // the bus; reactPlugin adds
|
|
2541
|
-
export const { useEmitter } = bus; //
|
|
2563
|
+
export const bus = createBus().use(reactPlugin()); // the bus; reactPlugin adds the React hooks
|
|
2564
|
+
export const { useEmitter, useEventState } = bus; // free hooks: they do not read `this`
|
|
2542
2565
|
```
|
|
2543
2566
|
|
|
2544
2567
|
```js
|
|
@@ -2568,25 +2591,24 @@ bus.on(
|
|
|
2568
2591
|
);
|
|
2569
2592
|
```
|
|
2570
2593
|
|
|
2571
|
-
Then the provider. It holds the state, calls `useEmitter` once, and
|
|
2594
|
+
Then the provider. It holds the state, calls `useEmitter` once to send the query to the bus, and `useEventState` once to receive the result. Every component below it only reads the context:
|
|
2572
2595
|
|
|
2573
2596
|
```jsx
|
|
2574
2597
|
// SearchProvider.jsx
|
|
2575
|
-
import { createContext, useContext,
|
|
2576
|
-
import {
|
|
2598
|
+
import { createContext, useContext, useState } from "react";
|
|
2599
|
+
import { useEmitter, useEventState } from "./bus";
|
|
2577
2600
|
import { queryChanged, searchLoaded } from "./search";
|
|
2578
2601
|
|
|
2579
2602
|
const SearchContext = createContext(null);
|
|
2580
2603
|
|
|
2581
2604
|
export function SearchProvider({ children }) {
|
|
2582
2605
|
const [query, setQuery] = useState("");
|
|
2583
|
-
const [items, setItems] = useState([]);
|
|
2584
2606
|
|
|
2585
2607
|
// React → bus: emits queryChanged(query) after every change of query, not on mount
|
|
2586
2608
|
useEmitter(queryChanged, query);
|
|
2587
2609
|
|
|
2588
|
-
// bus → React:
|
|
2589
|
-
|
|
2610
|
+
// bus → React: like useState([]), but searchLoaded sets the value; one subscription for the whole app
|
|
2611
|
+
const [items] = useEventState(searchLoaded, []);
|
|
2590
2612
|
|
|
2591
2613
|
return (
|
|
2592
2614
|
<SearchContext.Provider value={{ query, setQuery, items }}>
|
|
@@ -2620,8 +2642,9 @@ export function Results() {
|
|
|
2620
2642
|
}
|
|
2621
2643
|
```
|
|
2622
2644
|
|
|
2623
|
-
|
|
2645
|
+
Three limits to know:
|
|
2624
2646
|
|
|
2647
|
+
- **The provider must be mounted first.** `useEventState` is not a store: the bus keeps no values, so a result that arrives before the provider mounts, or after it unmounts, is lost. Put the provider at the root. If a value must survive that, keep it in a store below and write to it from a handler.
|
|
2625
2648
|
- **All consumers re-render** on any change of the context value. Split it into two contexts (`query` and `items`) if that is slow.
|
|
2626
2649
|
- **A new object counts as a change.** For a value that is rebuilt with the same content, pass `equals` in the options, the third argument, to say when two values are the same: `useEmitter(profileChanged, profile, { equals: (previous, next) => previous.id === next.id })`. The same object takes `traceId` and `traceGroupId`, which name the subscription and the flow in the [trace](tracing.md#show-which-subscription-began-a-chain).
|
|
2627
2650
|
|
|
@@ -3180,6 +3203,8 @@ export function Found() {
|
|
|
3180
3203
|
|
|
3181
3204
|
`setFound` is the setter of React. It changes the state of this component only and emits nothing.
|
|
3182
3205
|
|
|
3206
|
+
`useEventState` is not a store. Each component that calls it keeps its own copy of the last payload, and a component that mounts later does not see earlier events. To share a value between components, or to keep it for components that mount later, keep it in a state manager (Redux, Zustand, Jotai or another) and write to it from a handler of the event. See [Stores](stores.md). Without a state manager, one `useEventState` in a Context provider at the root shares the value with every component below it: [Plain React: Context, `useEmitter` and `useEventState`](stores.md#plain-react-context-useemitter-and-useeventstate).
|
|
3207
|
+
|
|
3183
3208
|
In TypeScript, the state type comes from the event: from `createAction<string[]>("search/loaded")` or from the event map of the bus. When the event has no payload type, as in the example above, it comes from `initial`: `["a"]` is `string[]`, and an empty `[]` is `unknown[]`. Give the event a type, or pass a typed `initial`, when you need more.
|
|
3184
3209
|
|
|
3185
3210
|
The bus keeps no values, so the hook cannot replay an event. An event emitted before the component mounts is not seen: if that matters, read the current value into `initial`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nanoeffects",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Cancel stale async work and fix race conditions in event handlers: a typed event bus where every handler is a task with an AbortSignal, plus latest, debounce, throttle, queue, retry, timeout and poll. About 1.4 kB gzip.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"event-bus",
|