nanoeffects 0.7.0 → 0.8.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/{action-creator-Bd4TBZgl.d.ts → action-creator-BrZF6hXc.d.ts} +0 -4
- package/dist/actions.d.ts +3 -3
- package/dist/actions.js +1 -1
- package/dist/{context-plugin-BVNKqkIx.d.ts → context-plugin--HkNN1ha.d.ts} +1 -1
- package/dist/dev.d.ts +9 -8
- package/dist/dev.js +1 -1
- package/dist/effects.d.ts +2 -2
- package/dist/effects.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/match.d.ts +50 -0
- package/dist/match.js +1 -0
- package/dist/{module-DZ5ngUrM.d.ts → module-DqKegXHt.d.ts} +43 -18
- package/dist/notrace/{action-creator-D-y_-xRS.d.ts → action-creator-DHAEgwuh.d.ts} +0 -4
- package/dist/notrace/actions.d.ts +52 -17
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/{context-plugin-DDfsaErA.d.ts → context-plugin-BapQkGwQ.d.ts} +1 -1
- package/dist/notrace/effects.d.ts +2 -2
- package/dist/notrace/effects.js +1 -1
- package/dist/notrace/index.d.ts +2 -2
- package/dist/notrace/{plugin-CDAngHwg.d.ts → plugin-Cxadz6uU.d.ts} +2 -2
- package/dist/notrace/policies.d.ts +1 -1
- package/dist/notrace/redux.d.ts +1 -1
- package/dist/observable.d.ts +1 -1
- package/dist/oop.d.ts +131 -0
- package/dist/oop.js +1 -0
- package/dist/{plugin-DQNPBcxx.d.ts → plugin-DkXCvsmB.d.ts} +2 -2
- package/dist/policies.d.ts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/redux.d.ts +1 -1
- package/dist/scope.d.ts +5 -6
- package/dist/trace.d.ts +1 -1
- package/dist/{types-BJ0fzV1p.d.ts → types-KavbvHix.d.ts} +13 -3
- package/docs/README.md +1 -0
- package/docs/actions.md +79 -35
- package/docs/guide.md +142 -18
- package/docs/hmr.md +6 -5
- package/docs/migrating-from-effector.md +49 -16
- package/docs/migrating-from-rxjs.md +17 -6
- package/docs/oop.md +286 -0
- package/docs/package.md +2 -0
- package/docs/plugins.md +227 -51
- package/docs/react.md +17 -5
- package/docs/scope.md +60 -22
- package/docs/stores.md +263 -109
- package/llms-full.txt +1152 -267
- package/llms.txt +1 -0
- package/package.json +20 -2
package/docs/stores.md
CHANGED
|
@@ -17,7 +17,7 @@ If you do not have a store yet, use `createBus` from the [Guide](guide.md).
|
|
|
17
17
|
- [React: sharing a value across components](#react-sharing-a-value-across-components)
|
|
18
18
|
- [Callbacks on every change](#callbacks-on-every-change)
|
|
19
19
|
- [Stop watching a store](#stop-watching-a-store)
|
|
20
|
-
- [
|
|
20
|
+
- [Change the store from any handler](#change-the-store-from-any-handler)
|
|
21
21
|
|
|
22
22
|
## Wire Redux Toolkit to a bus
|
|
23
23
|
|
|
@@ -28,23 +28,40 @@ Redux’s `dispatch` → reducers updates **state**. It does not give you first-
|
|
|
28
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).
|
|
29
29
|
|
|
30
30
|
```js
|
|
31
|
-
|
|
31
|
+
// bus.js
|
|
32
32
|
import { createBus } from "nanoeffects";
|
|
33
|
-
import { latest } from "nanoeffects/policies";
|
|
34
|
-
import { createEffectsMiddleware } from "nanoeffects/redux";
|
|
35
33
|
|
|
36
|
-
const
|
|
37
|
-
|
|
34
|
+
export const bus = createBus(); // the bus of your app; it can exist before the store
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
// user.js
|
|
39
|
+
import { createAction } from "@reduxjs/toolkit";
|
|
38
40
|
|
|
39
|
-
const
|
|
41
|
+
export const userRequested = createAction("user/requested"); // RTK
|
|
42
|
+
export const userLoaded = createAction("user/loaded");
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
// store.js
|
|
47
|
+
import { configureStore } from "@reduxjs/toolkit";
|
|
48
|
+
import { createEffectsMiddleware } from "nanoeffects/redux";
|
|
49
|
+
import { bus } from "./bus";
|
|
40
50
|
|
|
41
51
|
// createEffectsMiddleware — actions reach the bus after reducers; tasks get ctx.store
|
|
42
52
|
const effects = createEffectsMiddleware(bus);
|
|
43
53
|
|
|
44
|
-
const store = configureStore({
|
|
54
|
+
export const store = configureStore({
|
|
45
55
|
reducer,
|
|
46
56
|
middleware: (getDefault) => getDefault().concat(effects),
|
|
47
57
|
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
// user-handlers.js
|
|
62
|
+
import { latest } from "nanoeffects/policies";
|
|
63
|
+
import { bus } from "./bus";
|
|
64
|
+
import { userLoaded, userRequested } from "./user";
|
|
48
65
|
|
|
49
66
|
// latest — a newer run aborts the older one
|
|
50
67
|
bus.on(
|
|
@@ -95,28 +112,44 @@ const effects = createEffectsMiddleware(bus);
|
|
|
95
112
|
Zustand, MobX, Jotai, Valtio, Nanostores and Pinia have no adapter. Redux is different: use the middleware from the first section, it sends actions to the bus. A plain store works with the bus in two directions, and both are shown for each store below:
|
|
96
113
|
|
|
97
114
|
- **The store triggers an event.** The store tells you about a change with a callback. You pass that callback `bus.emitter(action)`, and the change becomes an event that starts a handler.
|
|
98
|
-
- **The event changes the store.** The handler reports its result as an event. A listener of the bus
|
|
115
|
+
- **The event changes the store.** The handler reports its result as an event. A listener of the bus gets the store from `ctx` and changes it with a mutation.
|
|
99
116
|
|
|
100
117
|
The example is a search box. The store holds two fields: `query`, which the user types, and `results`, which a request fills. A change of `query` starts a debounced search; the search ends with an event that writes `results`.
|
|
101
118
|
|
|
102
119
|
Writing `(query) => bus.emit(queryChanged(query))` for every subscription repeats the bus and the action. `emitterPlugin` adds `bus.emitter(action)`: it returns that callback for you. The action stays a plain `createAction`, so `bus.emit(queryChanged("nano"))` and `bus.on(queryChanged, …)` work as before.
|
|
103
120
|
|
|
104
|
-
The bus
|
|
121
|
+
The bus gets the store as context: `contextPlugin({ store })` puts it on every task as `ctx.store` ([Context fields](context.md)). A handler then changes the store with its mutations and never reaches for the store by name. Only `store.js` is different for each store below. It exports a `store` with the same three members:
|
|
122
|
+
|
|
123
|
+
- `setQuery(query)` and `setResults(items)` are the mutations. The user's input calls the first, the handler calls the second.
|
|
124
|
+
- `onQueryChanged(callback)` tells you about a change of `query`, and returns the function that stops it.
|
|
125
|
+
|
|
126
|
+
The bus, the events, the handlers and the link from the store to the bus are the same for every store, each in a file of its own: the bus knows no events, and the events know no bus.
|
|
105
127
|
|
|
106
128
|
```js
|
|
107
|
-
//
|
|
108
|
-
import { createBus } from "nanoeffects";
|
|
109
|
-
import {
|
|
110
|
-
import {
|
|
129
|
+
// bus.js
|
|
130
|
+
import { contextPlugin, createBus } from "nanoeffects";
|
|
131
|
+
import { emitterPlugin } from "nanoeffects/actions";
|
|
132
|
+
import { store } from "./store";
|
|
133
|
+
|
|
134
|
+
export const bus = createBus()
|
|
135
|
+
.use(emitterPlugin()) // adds bus.emitter
|
|
136
|
+
.use(contextPlugin({ store })); // ctx.store on every task
|
|
137
|
+
```
|
|
111
138
|
|
|
112
|
-
|
|
139
|
+
```js
|
|
140
|
+
// search.js
|
|
141
|
+
import { createAction } from "nanoeffects/actions";
|
|
113
142
|
|
|
114
143
|
// createAction — plain action creators; calling one only builds { type, payload }
|
|
115
144
|
export const queryChanged = createAction("search/queryChanged");
|
|
116
145
|
export const searchLoaded = createAction("search/loaded");
|
|
146
|
+
```
|
|
117
147
|
|
|
118
|
-
|
|
119
|
-
|
|
148
|
+
```js
|
|
149
|
+
// search-handlers.js
|
|
150
|
+
import { debounce, latest } from "nanoeffects/policies";
|
|
151
|
+
import { bus } from "./bus";
|
|
152
|
+
import { queryChanged, searchLoaded } from "./search";
|
|
120
153
|
|
|
121
154
|
// debounce — wait 300 ms of quiet; latest — abort the search of the previous query
|
|
122
155
|
bus.on(
|
|
@@ -130,141 +163,199 @@ bus.on(
|
|
|
130
163
|
}),
|
|
131
164
|
),
|
|
132
165
|
);
|
|
166
|
+
|
|
167
|
+
// event → store: the store comes from ctx, and a mutation changes it
|
|
168
|
+
bus.on(searchLoaded, (items, { store }) => {
|
|
169
|
+
store.setResults(items);
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
// connect.js
|
|
175
|
+
import { bus } from "./bus";
|
|
176
|
+
import { queryChanged } from "./search";
|
|
177
|
+
import { store } from "./store";
|
|
178
|
+
|
|
179
|
+
// store → event: bus.emitter turns the callback into an emit of queryChanged
|
|
180
|
+
export const stopQuery = store.onQueryChanged(bus.emitter(queryChanged));
|
|
133
181
|
```
|
|
134
182
|
|
|
135
183
|
Write `results` and never the field you watch: a handler that changed `query` would start itself again. Find your store below.
|
|
136
184
|
|
|
137
185
|
### Nanostores
|
|
138
186
|
|
|
139
|
-
`listen` calls back with the new value
|
|
187
|
+
`action` makes a named mutation of an atom. `listen` calls back with the new value; `subscribe` would also call at once, with the current value, so use `listen`.
|
|
140
188
|
|
|
141
189
|
```js
|
|
142
|
-
|
|
143
|
-
import {
|
|
190
|
+
// store.js
|
|
191
|
+
import { action, atom } from "nanostores";
|
|
144
192
|
|
|
145
193
|
const $query = atom("");
|
|
146
194
|
const $results = atom([]);
|
|
147
195
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
$
|
|
154
|
-
}
|
|
196
|
+
export const store = {
|
|
197
|
+
// action — a named mutation of an atom
|
|
198
|
+
setQuery: action($query, "setQuery", (target, query) => target.set(query)),
|
|
199
|
+
setResults: action($results, "setResults", (target, items) => target.set(items)),
|
|
200
|
+
// listen — calls back with the new query on every change
|
|
201
|
+
onQueryChanged: (callback) => $query.listen(callback),
|
|
202
|
+
};
|
|
155
203
|
```
|
|
156
204
|
|
|
157
205
|
### MobX
|
|
158
206
|
|
|
159
|
-
`reaction` watches an expression and calls back with its value.
|
|
207
|
+
`makeAutoObservable` turns the methods of a class into actions, which is where MobX wants changes to happen. `reaction` watches an expression and calls back with its value.
|
|
208
|
+
|
|
209
|
+
<!-- docs-check: fork -->
|
|
160
210
|
|
|
161
211
|
```js
|
|
162
|
-
|
|
163
|
-
import {
|
|
212
|
+
// store.js
|
|
213
|
+
import { makeAutoObservable, reaction } from "mobx";
|
|
164
214
|
|
|
165
|
-
|
|
215
|
+
class SearchStore {
|
|
216
|
+
query = "";
|
|
217
|
+
results = [];
|
|
166
218
|
|
|
167
|
-
|
|
168
|
-
|
|
219
|
+
constructor() {
|
|
220
|
+
makeAutoObservable(this); // the methods below become actions
|
|
221
|
+
}
|
|
169
222
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
223
|
+
setQuery(query) {
|
|
224
|
+
this.query = query;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
setResults(items) {
|
|
228
|
+
this.results = items;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// reaction — watches the expression and calls back with its value
|
|
232
|
+
onQueryChanged(callback) {
|
|
233
|
+
return reaction(() => this.query, callback);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export const store = new SearchStore();
|
|
176
238
|
```
|
|
177
239
|
|
|
178
240
|
### Valtio
|
|
179
241
|
|
|
180
|
-
`subscribeKey` watches one field of a proxy state.
|
|
242
|
+
`subscribeKey` watches one field of a proxy state. A mutation assigns to the proxy.
|
|
243
|
+
|
|
244
|
+
<!-- docs-check: fork -->
|
|
181
245
|
|
|
182
246
|
```js
|
|
247
|
+
// store.js
|
|
183
248
|
import { proxy } from "valtio";
|
|
184
249
|
import { subscribeKey } from "valtio/utils";
|
|
185
|
-
import { bus, emitQueryChanged, searchLoaded } from "./search-bus";
|
|
186
250
|
|
|
187
251
|
const state = proxy({ query: "", results: [] });
|
|
188
252
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
253
|
+
export const store = {
|
|
254
|
+
setQuery(query) {
|
|
255
|
+
state.query = query;
|
|
256
|
+
},
|
|
257
|
+
setResults(items) {
|
|
258
|
+
state.results = items;
|
|
259
|
+
},
|
|
260
|
+
// subscribeKey — watches one field
|
|
261
|
+
onQueryChanged: (callback) => subscribeKey(state, "query", callback),
|
|
262
|
+
};
|
|
196
263
|
```
|
|
197
264
|
|
|
198
265
|
### Jotai
|
|
199
266
|
|
|
200
|
-
`sub` gives no value, so read it yourself.
|
|
267
|
+
`sub` gives no value, so read it yourself. A write-only atom is the mutation, and `set` runs it.
|
|
268
|
+
|
|
269
|
+
<!-- docs-check: fork -->
|
|
201
270
|
|
|
202
271
|
```js
|
|
272
|
+
// store.js
|
|
203
273
|
import { atom, createStore } from "jotai";
|
|
204
|
-
import { bus, emitQueryChanged, searchLoaded } from "./search-bus";
|
|
205
274
|
|
|
206
275
|
const queryAtom = atom("");
|
|
207
276
|
const resultsAtom = atom([]);
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
//
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
277
|
+
const jotai = createStore();
|
|
278
|
+
|
|
279
|
+
// write-only atoms: the mutations
|
|
280
|
+
const setQueryAtom = atom(null, (_get, set, query) => set(queryAtom, query));
|
|
281
|
+
const setResultsAtom = atom(null, (_get, set, items) => set(resultsAtom, items));
|
|
282
|
+
|
|
283
|
+
export const store = {
|
|
284
|
+
setQuery: (query) => jotai.set(setQueryAtom, query),
|
|
285
|
+
setResults: (items) => jotai.set(setResultsAtom, items),
|
|
286
|
+
// sub calls back without a value; get reads it
|
|
287
|
+
onQueryChanged: (callback) =>
|
|
288
|
+
jotai.sub(queryAtom, () => callback(jotai.get(queryAtom))),
|
|
289
|
+
};
|
|
217
290
|
```
|
|
218
291
|
|
|
219
292
|
### Zustand
|
|
220
293
|
|
|
221
|
-
With `subscribeWithSelector` the selector picks the field, and the callback is called only when it changes. Without a selector Zustand calls back on every `setState`, with the whole state.
|
|
294
|
+
With `subscribeWithSelector` the selector picks the field, and the callback is called only when it changes. Without a selector Zustand calls back on every `setState`, with the whole state. The mutations are functions in the state.
|
|
295
|
+
|
|
296
|
+
<!-- docs-check: fork -->
|
|
222
297
|
|
|
223
298
|
```js
|
|
299
|
+
// store.js
|
|
224
300
|
import { create } from "zustand";
|
|
225
301
|
import { subscribeWithSelector } from "zustand/middleware";
|
|
226
|
-
import { bus, emitQueryChanged, searchLoaded } from "./search-bus";
|
|
227
302
|
|
|
228
|
-
const
|
|
229
|
-
subscribeWithSelector(() => ({
|
|
303
|
+
const useSearch = create(
|
|
304
|
+
subscribeWithSelector((set) => ({
|
|
305
|
+
query: "",
|
|
306
|
+
results: [],
|
|
307
|
+
// the mutations are part of the state
|
|
308
|
+
setQuery: (query) => set({ query }),
|
|
309
|
+
setResults: (items) => set({ results: items }),
|
|
310
|
+
})),
|
|
230
311
|
);
|
|
231
312
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
});
|
|
313
|
+
export const store = {
|
|
314
|
+
setQuery: (query) => useSearch.getState().setQuery(query),
|
|
315
|
+
setResults: (items) => useSearch.getState().setResults(items),
|
|
316
|
+
// a selector: called back with state.query only when it changes
|
|
317
|
+
onQueryChanged: (callback) => useSearch.subscribe((state) => state.query, callback),
|
|
318
|
+
};
|
|
239
319
|
```
|
|
240
320
|
|
|
241
321
|
### Pinia
|
|
242
322
|
|
|
243
|
-
Pinia (the store of Vue) keeps reactive objects, and its `$subscribe` reports every mutation of the store, not one field. Watch one getter with Vue's `watch` instead: it calls back with the new value.
|
|
323
|
+
Pinia (the store of Vue) keeps reactive objects, and its `$subscribe` reports every mutation of the store, not one field. Watch one getter with Vue's `watch` instead: it calls back with the new value. The mutations are the actions of the store.
|
|
324
|
+
|
|
325
|
+
<!-- docs-check: fork -->
|
|
244
326
|
|
|
245
327
|
```js
|
|
246
|
-
|
|
328
|
+
// store.js
|
|
247
329
|
import { defineStore } from "pinia";
|
|
248
|
-
import {
|
|
330
|
+
import { watch } from "vue";
|
|
249
331
|
|
|
250
|
-
// defineStore — Pinia: the state of one store
|
|
332
|
+
// defineStore — Pinia: the state and the actions of one store
|
|
251
333
|
const useSearch = defineStore("search", {
|
|
252
334
|
state: () => ({ query: "", results: [] }),
|
|
335
|
+
actions: {
|
|
336
|
+
setQuery(query) {
|
|
337
|
+
this.query = query;
|
|
338
|
+
},
|
|
339
|
+
setResults(items) {
|
|
340
|
+
this.results = items;
|
|
341
|
+
},
|
|
342
|
+
},
|
|
253
343
|
});
|
|
254
344
|
|
|
255
345
|
const search = useSearch(); // call it after the app has installed Pinia
|
|
256
346
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
});
|
|
347
|
+
export const store = {
|
|
348
|
+
setQuery: (query) => search.setQuery(query),
|
|
349
|
+
setResults: (items) => search.setResults(items),
|
|
350
|
+
// watch — calls back with the new query when search.query changes
|
|
351
|
+
onQueryChanged: (callback) => watch(() => search.query, callback),
|
|
352
|
+
};
|
|
264
353
|
```
|
|
265
354
|
|
|
266
355
|
### React: sharing a value across components
|
|
267
356
|
|
|
357
|
+
<!-- docs-check: new -->
|
|
358
|
+
|
|
268
359
|
A value of your UI (a query, a filter, a name) often has two jobs. Several components read it and re-render when it changes, and the bus must hear about each change to start a handler. The recipes below differ in where the value lives. Pick the first one unless a handler must read the value.
|
|
269
360
|
|
|
270
361
|
| The value lives in | Use when |
|
|
@@ -277,20 +368,30 @@ A value of your UI (a query, a filter, a name) often has two jobs. Several compo
|
|
|
277
368
|
|
|
278
369
|
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.
|
|
279
370
|
|
|
280
|
-
First the bus. `reactPlugin()` adds `bus.useEmitter`, and destructuring it gives a plain hook that you export:
|
|
371
|
+
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:
|
|
281
372
|
|
|
282
373
|
```js
|
|
283
|
-
//
|
|
374
|
+
// bus.js
|
|
284
375
|
import { createBus } from "nanoeffects";
|
|
285
|
-
import { createAction } from "nanoeffects/actions";
|
|
286
|
-
import { debounce, latest } from "nanoeffects/policies";
|
|
287
376
|
import { reactPlugin } from "nanoeffects/react";
|
|
288
377
|
|
|
289
378
|
export const bus = createBus().use(reactPlugin()); // the bus; reactPlugin adds bus.useEmitter
|
|
290
379
|
export const { useEmitter } = bus; // a free hook: it does not read `this`
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
```js
|
|
383
|
+
// search.js
|
|
384
|
+
import { createAction } from "nanoeffects/actions";
|
|
291
385
|
|
|
292
386
|
export const queryChanged = createAction("query/changed");
|
|
293
387
|
export const searchLoaded = createAction("search/loaded");
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
```js
|
|
391
|
+
// search-handlers.js
|
|
392
|
+
import { debounce, latest } from "nanoeffects/policies";
|
|
393
|
+
import { bus } from "./bus";
|
|
394
|
+
import { queryChanged, searchLoaded } from "./search";
|
|
294
395
|
|
|
295
396
|
// debounce — wait 300 ms of quiet; latest — a newer query aborts the older request
|
|
296
397
|
bus.on(
|
|
@@ -310,7 +411,8 @@ Then the provider. It holds the state, calls `useEmitter` once, and listens for
|
|
|
310
411
|
```jsx
|
|
311
412
|
// SearchProvider.jsx
|
|
312
413
|
import { createContext, useContext, useEffect, useState } from "react";
|
|
313
|
-
import { bus,
|
|
414
|
+
import { bus, useEmitter } from "./bus";
|
|
415
|
+
import { queryChanged, searchLoaded } from "./search";
|
|
314
416
|
|
|
315
417
|
const SearchContext = createContext(null);
|
|
316
418
|
|
|
@@ -367,6 +469,8 @@ Two limits to know:
|
|
|
367
469
|
|
|
368
470
|
A context is not visible to a bus handler. When a handler needs the current value, keep the value outside React. About twelve lines of `useSyncExternalStore` do it, and `contextPlugin` hands it to every handler as `ctx.query`:
|
|
369
471
|
|
|
472
|
+
<!-- docs-check: new -->
|
|
473
|
+
|
|
370
474
|
```jsx
|
|
371
475
|
// value-store.js
|
|
372
476
|
import { useSyncExternalStore } from "react";
|
|
@@ -396,19 +500,36 @@ export function createValue(initial) {
|
|
|
396
500
|
export const useValue = (store) => useSyncExternalStore(store.subscribe, store.get);
|
|
397
501
|
```
|
|
398
502
|
|
|
399
|
-
```
|
|
400
|
-
//
|
|
401
|
-
import {
|
|
402
|
-
import { createAction, emitterPlugin } from "nanoeffects/actions";
|
|
403
|
-
import { createValue, useValue } from "./value-store";
|
|
503
|
+
```js
|
|
504
|
+
// query.js
|
|
505
|
+
import { createValue } from "./value-store";
|
|
404
506
|
|
|
405
|
-
const query = createValue("");
|
|
507
|
+
export const query = createValue("");
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
```js
|
|
511
|
+
// bus.js
|
|
512
|
+
import { contextPlugin, createBus } from "nanoeffects";
|
|
513
|
+
import { emitterPlugin } from "nanoeffects/actions";
|
|
514
|
+
import { query } from "./query";
|
|
406
515
|
|
|
407
516
|
// emitterPlugin adds bus.emitter; contextPlugin puts `query` on every task as ctx.query
|
|
408
|
-
const bus = createBus().use(emitterPlugin()).use(contextPlugin({ query }));
|
|
517
|
+
export const bus = createBus().use(emitterPlugin()).use(contextPlugin({ query }));
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
```js
|
|
521
|
+
// search.js
|
|
522
|
+
import { createAction } from "nanoeffects/actions";
|
|
409
523
|
|
|
410
|
-
const queryChanged = createAction("query/changed");
|
|
411
|
-
const refreshed = createAction("search/refreshed");
|
|
524
|
+
export const queryChanged = createAction("query/changed");
|
|
525
|
+
export const refreshed = createAction("search/refreshed");
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
```js
|
|
529
|
+
// search-handlers.js
|
|
530
|
+
import { bus } from "./bus";
|
|
531
|
+
import { query } from "./query";
|
|
532
|
+
import { queryChanged, refreshed } from "./search";
|
|
412
533
|
|
|
413
534
|
// store → bus: bus.emitter turns the action into a callback; one place, any number of setters
|
|
414
535
|
const emitQueryChanged = bus.emitter(queryChanged);
|
|
@@ -419,6 +540,12 @@ bus.on(refreshed, async (_payload, ctx) => {
|
|
|
419
540
|
const items = await api.search(ctx.query.get(), { signal: ctx.signal });
|
|
420
541
|
console.log(items.length);
|
|
421
542
|
});
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
```jsx
|
|
546
|
+
// SearchBox.jsx
|
|
547
|
+
import { query } from "./query";
|
|
548
|
+
import { useValue } from "./value-store";
|
|
422
549
|
|
|
423
550
|
// any number of components read and write the same value
|
|
424
551
|
export function SearchBox() {
|
|
@@ -431,16 +558,31 @@ export function SearchBox() {
|
|
|
431
558
|
|
|
432
559
|
With Nanostores, Jotai or Zustand the value already lives outside React and has a subscription. Connect it to the bus once, as in [Nanostores](#nanostores), and read it in components with the library's own hook:
|
|
433
560
|
|
|
561
|
+
<!-- docs-check: new -->
|
|
562
|
+
|
|
563
|
+
```js
|
|
564
|
+
// bus.js
|
|
565
|
+
import { createBus } from "nanoeffects";
|
|
566
|
+
import { emitterPlugin } from "nanoeffects/actions";
|
|
567
|
+
|
|
568
|
+
export const bus = createBus().use(emitterPlugin()); // adds bus.emitter
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
```js
|
|
572
|
+
// search.js
|
|
573
|
+
import { createAction } from "nanoeffects/actions";
|
|
574
|
+
|
|
575
|
+
export const queryChanged = createAction("query/changed");
|
|
576
|
+
```
|
|
577
|
+
|
|
434
578
|
```jsx
|
|
435
|
-
//
|
|
579
|
+
// SearchBox.jsx
|
|
436
580
|
import { atom } from "nanostores";
|
|
437
581
|
import { useStore } from "@nanostores/react";
|
|
438
|
-
import {
|
|
439
|
-
import {
|
|
582
|
+
import { bus } from "./bus";
|
|
583
|
+
import { queryChanged } from "./search";
|
|
440
584
|
|
|
441
|
-
const bus = createBus().use(emitterPlugin()); // adds bus.emitter
|
|
442
585
|
const $query = atom("");
|
|
443
|
-
const queryChanged = createAction("query/changed");
|
|
444
586
|
|
|
445
587
|
// listen, not subscribe: listen skips the call with the current value at start
|
|
446
588
|
$query.listen(bus.emitter(queryChanged));
|
|
@@ -455,16 +597,28 @@ export function SearchBox() {
|
|
|
455
597
|
|
|
456
598
|
React may run a reducer more than once for one dispatch (twice in StrictMode), so a `bus.emit` inside it fires twice. Dispatch to the reducer first, then emit, from one helper. Or keep the state in a reducer and pass it to `useEmitter` like any other value.
|
|
457
599
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
600
|
+
<!-- docs-check: new -->
|
|
601
|
+
|
|
602
|
+
```js
|
|
603
|
+
// bus.js
|
|
461
604
|
import { createBus } from "nanoeffects";
|
|
605
|
+
|
|
606
|
+
export const bus = createBus(); // the bus: created once
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
```js
|
|
610
|
+
// search.js
|
|
462
611
|
import { createAction } from "nanoeffects/actions";
|
|
463
612
|
|
|
464
|
-
const
|
|
613
|
+
export const queryChanged = createAction("query/changed");
|
|
614
|
+
export const queryCleared = createAction("query/cleared");
|
|
615
|
+
```
|
|
465
616
|
|
|
466
|
-
|
|
467
|
-
|
|
617
|
+
```jsx
|
|
618
|
+
// Search.jsx
|
|
619
|
+
import { useReducer } from "react";
|
|
620
|
+
import { bus } from "./bus";
|
|
621
|
+
import { queryChanged, queryCleared } from "./search";
|
|
468
622
|
|
|
469
623
|
function reducer(state, action) {
|
|
470
624
|
if (queryChanged.match(action)) {
|
|
@@ -504,8 +658,8 @@ The stores above call back only when the value changed, so the emitter does not
|
|
|
504
658
|
|
|
505
659
|
### Stop watching a store
|
|
506
660
|
|
|
507
|
-
|
|
661
|
+
`connect.js` keeps the function that `onQueryChanged` returned (`stopQuery`). Call it yourself, or hand it over: `onAbort(ctx, stopQuery)` inside a task, or `m.signal.addEventListener("abort", stopQuery)` in a module. To stop the other direction, remove the listener of `searchLoaded` with `bus.off(searchLoaded, handler)`, or register it in a module, which removes it on unmount.
|
|
508
662
|
|
|
509
|
-
###
|
|
663
|
+
### Change the store from any handler
|
|
510
664
|
|
|
511
|
-
|
|
665
|
+
A handler gets the store from `ctx.store` and calls a mutation, whatever it handles: not only the result of a search. The same rule holds: write the field you do not watch. For a MobX `flow`, `onAbort(ctx, () => promise.cancel())` cancels it with the task.
|