bunja 2.1.1 → 3.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -69,6 +69,26 @@ function MyComponent() {
69
69
  }
70
70
  ```
71
71
 
72
+ #### Seeding the first instance
73
+
74
+ If a bunja needs creation-time data, use `bunja.withSeed`. A default seed is
75
+ required when the bunja is declared, so callers may still omit the seed.
76
+
77
+ The seed is used only when a matching bunja instance is first created. It is not
78
+ part of the bunja instance identity. If the matching instance already exists,
79
+ later seeds are ignored.
80
+
81
+ Seeds can be supplied to `store.get`, `useBunja`, `bunja.use`, or `bunja.will`.
82
+
83
+ ```ts
84
+ const formBunja = bunja.withSeed({ title: "" }, (seed) => {
85
+ const titleAtom = atom(seed.title);
86
+ return { titleAtom };
87
+ });
88
+
89
+ useBunja({ bunja: formBunja, seed: { title: "Draft" } });
90
+ ```
91
+
72
92
  ### Defining a Bunja that relies on other Bunja
73
93
 
74
94
  If you want to manage a state with a broad lifetime and another state with a
@@ -156,6 +176,112 @@ either `resourceFooBunja` or `resourceBarBunja`, since they depend on
156
176
  >
157
177
  > See: <https://github.com/facebook/react/issues/16728>
158
178
 
179
+ #### Bunja init rules
180
+
181
+ Inside a bunja initialization function, call `bunja.use` and `bunja.will`
182
+ unconditionally and in the same order every time, similar to React's
183
+ [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks). Do not put
184
+ them inside `if` statements, loops, or callbacks.
185
+
186
+ The target passed to `bunja.use` or `bunja.will` must be static. If you pass
187
+ scope value pairs as the second argument or through a bunja ref's `with` field,
188
+ the list of scope bindings must also be static. Do not choose a different bunja
189
+ or add/remove scope bindings based on runtime conditions.
190
+
191
+ `seed` is the exception. A bunja ref may provide a dynamic `seed` value because
192
+ seed is used only when creating the first matching bunja instance and is not
193
+ recorded in the dependency graph.
194
+
195
+ ```ts
196
+ const consumerBunja = bunja.withSeed({ currentUserId: "" }, (seed) => {
197
+ const getProfile = bunja.will({
198
+ // Must stay the same on every init.
199
+ bunja: profileBunja,
200
+ with: [
201
+ // Must stay the same on every init.
202
+ LocaleScope.bind("en-US"),
203
+ ],
204
+ // May change for each first instance creation.
205
+ seed: { currentUserId: seed.currentUserId },
206
+ });
207
+
208
+ return getProfile();
209
+ });
210
+ ```
211
+
212
+ If a dependency should only be used for one branch, declare it with `bunja.will`
213
+ unconditionally, then branch on whether to call the returned thunk.
214
+
215
+ #### Conditional dependencies
216
+
217
+ Use `bunja.will` when a bunja may depend on another bunja only for a selected
218
+ branch. `bunja.will` declares a possible dependency and returns a thunk. Only a
219
+ called thunk becomes an active dependency and is mounted.
220
+
221
+ The thunk may only be called during the same bunja initialization function that
222
+ created it.
223
+
224
+ ```ts
225
+ const resourceA = bunja(() => {
226
+ const { send } = bunja.use(websocketBunja);
227
+ bunja.effect(() => {
228
+ send("subscribe-a");
229
+ return () => send("unsubscribe-a");
230
+ });
231
+ });
232
+
233
+ const resourceB = bunja(() => {
234
+ const { send } = bunja.use(websocketBunja);
235
+ bunja.effect(() => {
236
+ send("subscribe-b");
237
+ return () => send("unsubscribe-b");
238
+ });
239
+ });
240
+
241
+ const selectedResourceBunja = bunja(() => {
242
+ const selected = bunja.use(SelectedResourceScope);
243
+ const useA = bunja.will(resourceA);
244
+ const useB = bunja.will(resourceB);
245
+
246
+ return selected === "a" ? useA() : useB();
247
+ });
248
+ ```
249
+
250
+ When a `bunja.will` thunk is called, the selected dependency becomes part of the
251
+ current bunja instance. If that selected dependency resolves to a different
252
+ instance because of scope values, the current bunja also resolves to a different
253
+ instance.
254
+
255
+ A declared but uncalled `bunja.will` dependency has no effect on the current
256
+ bunja instance.
257
+
258
+ #### Prebaking the dependency graph
259
+
260
+ During normal `store.get` and `useBunja`, only the selected `bunja.will` branch
261
+ is baked. If a declared `bunja.will` dependency is not called, that dependency's
262
+ own dependencies may still be unknown. `store.prebake` can be used by devtools
263
+ or debugging code to visit those declared dependencies and fill in the graph.
264
+
265
+ Prebaking runs bunja init functions in a dry graph-collection mode. It does not
266
+ create ref-counted bunja instances, it does not mount dependencies, and it does
267
+ not run `bunja.effect` callbacks. Prebake still calls bunja init functions, so
268
+ put external resource creation and subscriptions inside `bunja.effect` if they
269
+ must not run during graph collection.
270
+
271
+ `store.prebake` rejects root bunja refs that provide a seed. During dry-run
272
+ initialization, bunja refs are converted to graph refs, so every prebaked bunja
273
+ is initialized with its declared default seed.
274
+
275
+ ```ts
276
+ const result = store.prebake(selectedResourceBunja, readScope);
277
+
278
+ result.relatedBunjas;
279
+ result.requiredScopes;
280
+ ```
281
+
282
+ The normal `store.get` and `useBunja` paths do not prebake automatically. They
283
+ still mount only the active `bunja.will` branch.
284
+
159
285
  ### Dependency injection using Scope
160
286
 
161
287
  You can use a bunja for local state management.\
@@ -239,6 +365,11 @@ const UrlContext = createContext("https://example.com/");
239
365
  const UrlScope = createScopeFromContext(UrlContext);
240
366
  ```
241
367
 
368
+ When using React 19, Bunja reads scope contexts lazily with `React.use`, so only
369
+ the contexts needed by the active branch are read. With React 18, Bunja must
370
+ read all bound contexts before resolving the bunja because `useContext` cannot
371
+ be called conditionally. In React 18, call `bindScope` before rendering.
372
+
242
373
  #### Injecting dependencies directly into the scope
243
374
 
244
375
  You might want to use a bunja directly within a React component where the values
@@ -260,15 +391,15 @@ function MyComponent() {
260
391
 
261
392
  ##### Doing the same thing inside a bunja
262
393
 
263
- You can use `bunja.fork` to inject scope values from within a bunja
264
- initialization function.
394
+ You can pass scope value pairs as the second argument to `bunja.use` to override
395
+ scope values from within a bunja initialization function.
265
396
 
266
397
  ```ts
267
398
  const myBunja = bunja(() => {
268
- const fooData = bunja.fork(fetchBunja, [
399
+ const fooData = bunja.use(fetchBunja, [
269
400
  UrlScope.bind("https://example.com/foo"),
270
401
  ]);
271
- const barData = bunja.fork(fetchBunja, [
402
+ const barData = bunja.use(fetchBunja, [
272
403
  UrlScope.bind("https://example.com/bar"),
273
404
  ]);
274
405