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 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 `useEmitter`
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 it gives a plain hook that you export from the bus file:
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 bus.useEmitter
384
- export const { useEmitter } = bus; // a free hook: it does not read `this`
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 listens for the result. Every component below it only reads the context:
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, useEffect, useState } from "react";
419
- import { bus, useEmitter } from "./bus";
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: bus.on returns the unsubscribe function, which the effect returns
432
- useEffect(() => bus.on(searchLoaded, setItems), []);
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
- Two limits to know:
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 `useEmitter`
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 it gives a plain hook that you export from the bus file:
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 bus.useEmitter
2541
- export const { useEmitter } = bus; // a free hook: it does not read `this`
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 listens for the result. Every component below it only reads the context:
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, useEffect, useState } from "react";
2576
- import { bus, useEmitter } from "./bus";
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: bus.on returns the unsubscribe function, which the effect returns
2589
- useEffect(() => bus.on(searchLoaded, setItems), []);
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
- Two limits to know:
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.18.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",