r-state-tree 0.8.0 → 0.8.1

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
@@ -139,7 +139,7 @@ app.todo.title;
139
139
 
140
140
  - Always create with `createStore()` and attach with `mount()`. Stores cannot be constructed with `new` directly.
141
141
  - Prefer no custom constructor. Type stores as `Store<Props>` and access props via `this.props`.
142
- - Constructors may register framework-owned reactions and effects after `super(props)`. Gate external resource acquisition on `isMounted` and return cleanup from effects.
142
+ - Constructors may register framework-owned reactions and effects after `super(props)`. Store registrations activate when mounting and are disposed with the store; return cleanup from effects to release resources.
143
143
  - Do not shadow or re-declare `props` as a class field; `props` is read-only. Use the generic `Store<{ ... }>` for typing.
144
144
 
145
145
  ```ts
@@ -212,7 +212,6 @@ class TodoStore extends Store {
212
212
  constructor(props) {
213
213
  super(props);
214
214
  this.effect(() => {
215
- if (!this.isMounted) return;
216
215
  const connection = connect();
217
216
  return () => connection.close();
218
217
  });
@@ -243,6 +242,10 @@ class TodoStore extends Store {
243
242
  }
244
243
  ```
245
244
 
245
+ `Store.effect()` and `Store.reaction()` register mount-scoped behavior. Mounting first links the complete store tree and marks it mounted bottom-up in one transaction. Registrations then activate bottom-up, so even a grandchild's initial effect observes every ancestor and descendant as mounted. A reaction establishes its initial value at activation and still skips its initial callback. Calling the disposer returned during construction cancels the pending registration; after mount, it disposes the live subscription. All registrations are disposed automatically with the store.
246
+
247
+ Use the exported standalone `effect()` or `reaction()` when observing a store from outside its owned lifetime—for example, to observe the public reactive `isMounted` transition to `false`.
248
+
246
249
  ### Context
247
250
 
248
251
  Share data across the store tree without prop drilling:
@@ -1217,7 +1220,8 @@ class S extends Store {
1217
1220
  constructor(props) {
1218
1221
  super(props);
1219
1222
  this.effect(() => {
1220
- if (!this.isMounted) return;
1223
+ const resource = acquireResource();
1224
+ return () => resource.dispose();
1221
1225
  });
1222
1226
  }
1223
1227
  }
@@ -1281,7 +1285,7 @@ When child stores are created during mount with `models` that point back into th
1281
1285
  - Core: `createStore`, `mount`, `Symbol.dispose`, `updateStore`
1282
1286
  - Snapshots: `onSnapshot`, `toSnapshot`, `applySnapshot`, `onSnapshotDiff`
1283
1287
  - Lifecycle: reactive `Store.isMounted`, reactive `Model.parent`, and owned `reaction`/`effect`
1284
- - Best practices: domain in Models; delegate from Stores; stable keys for `@child`; gate resources on lifecycle state; don’t shadow `props`.
1288
+ - Best practices: domain in Models; delegate from Stores; stable keys for `@child`; return cleanup from store effects; don’t shadow `props`.
1285
1289
 
1286
1290
  ## Testing
1287
1291
 
@@ -2036,7 +2036,7 @@ class StoreAdministration extends PreactObjectAdministration {
2036
2036
  disposed = false;
2037
2037
  contextCache = /* @__PURE__ */ new Map();
2038
2038
  childStoreDataMap = /* @__PURE__ */ new Map();
2039
- ownedDisposers = /* @__PURE__ */ new Set();
2039
+ reactiveRegistrations = /* @__PURE__ */ new Set();
2040
2040
  configurationGetter;
2041
2041
  setConfiguration(configurationGetter) {
2042
2042
  this.configurationGetter = configurationGetter;
@@ -2060,7 +2060,9 @@ class StoreAdministration extends PreactObjectAdministration {
2060
2060
  }
2061
2061
  });
2062
2062
  childStoreData.value.set(stores);
2063
- stores.forEach((s) => getStoreAdm(s).mount(this, name));
2063
+ if (this.isMounted) {
2064
+ stores.forEach((s) => getStoreAdm(s).mount(this, name));
2065
+ }
2064
2066
  return stores;
2065
2067
  }
2066
2068
  const newStores = /* @__PURE__ */ new Set();
@@ -2111,7 +2113,9 @@ class StoreAdministration extends PreactObjectAdministration {
2111
2113
  signalsCore.batch(() => childStoreData.value.set(stores));
2112
2114
  }
2113
2115
  removedStores.forEach((s) => getStoreAdm(s).dispose(true));
2114
- newStores.forEach((s) => getStoreAdm(s).mount(this, name));
2116
+ if (this.isMounted) {
2117
+ newStores.forEach((s) => getStoreAdm(s).mount(this, name));
2118
+ }
2115
2119
  return stores;
2116
2120
  }
2117
2121
  setSingleStore(name, element) {
@@ -2130,7 +2134,9 @@ class StoreAdministration extends PreactObjectAdministration {
2130
2134
  }
2131
2135
  const childStore = this.createChildStore(element);
2132
2136
  signalsCore.batch(() => childStoreData.value.set(childStore));
2133
- getStoreAdm(childStore).mount(this, name);
2137
+ if (this.isMounted) {
2138
+ getStoreAdm(childStore).mount(this, name);
2139
+ }
2134
2140
  return childStore;
2135
2141
  } else {
2136
2142
  signalsCore.batch(() => updateProps(oldStore.props, props));
@@ -2223,31 +2229,37 @@ class StoreAdministration extends PreactObjectAdministration {
2223
2229
  }
2224
2230
  return void 0;
2225
2231
  }
2226
- own(disposer) {
2232
+ register(start) {
2227
2233
  if (this.disposed) {
2228
- disposer();
2229
2234
  throw new Error(
2230
2235
  "r-state-tree: cannot register reactive resources on a disposed store"
2231
2236
  );
2232
2237
  }
2233
- let active = true;
2234
- const wrapped = () => {
2235
- if (!active) return;
2236
- active = false;
2237
- this.ownedDisposers.delete(wrapped);
2238
- disposer();
2238
+ const registration = { start, disposed: false };
2239
+ this.reactiveRegistrations.add(registration);
2240
+ if (this.isMounted) registration.stop = registration.start();
2241
+ return () => {
2242
+ if (registration.disposed) return;
2243
+ registration.disposed = true;
2244
+ this.reactiveRegistrations.delete(registration);
2245
+ registration.stop?.();
2246
+ registration.stop = void 0;
2239
2247
  };
2240
- this.ownedDisposers.add(wrapped);
2241
- return wrapped;
2248
+ }
2249
+ startReactiveRegistrations() {
2250
+ this.reactiveRegistrations.forEach((registration) => {
2251
+ if (!registration.disposed && !registration.stop) {
2252
+ registration.stop = registration.start();
2253
+ }
2254
+ });
2242
2255
  }
2243
2256
  reaction(track, callback) {
2244
- const unsub = reaction(track, callback);
2245
- return this.own(unsub);
2257
+ return this.register(() => reaction(track, callback));
2246
2258
  }
2247
2259
  effect(callback) {
2248
- return this.own(signalsCore.effect(callback));
2260
+ return this.register(() => signalsCore.effect(callback));
2249
2261
  }
2250
- mount(parent = null, childName) {
2262
+ mount(parent = null, childName, activationQueue) {
2251
2263
  if (this.disposed) {
2252
2264
  throw new Error("r-state-tree: cannot mount a disposed store");
2253
2265
  }
@@ -2263,19 +2275,29 @@ class StoreAdministration extends PreactObjectAdministration {
2263
2275
  throw createCircularMountError(frame);
2264
2276
  }
2265
2277
  mountingStack.push(frame);
2278
+ const isOutermostMount = activationQueue === void 0;
2279
+ const registrationsToStart = activationQueue ?? [];
2266
2280
  try {
2267
2281
  signalsCore.batch(() => {
2268
2282
  this.parent = parent || null;
2269
2283
  this.childStoreDataMap.forEach(({ value }, name) => {
2270
2284
  const stores = value.get();
2271
2285
  if (Array.isArray(stores)) {
2272
- stores?.forEach((s) => getStoreAdm(s)?.mount(this, name));
2286
+ stores?.forEach(
2287
+ (s) => getStoreAdm(s)?.mount(this, name, registrationsToStart)
2288
+ );
2273
2289
  } else if (stores) {
2274
- getStoreAdm(stores)?.mount(this, name);
2290
+ getStoreAdm(stores)?.mount(this, name, registrationsToStart);
2275
2291
  }
2276
2292
  });
2277
2293
  this.mountedSignal.set(true);
2294
+ registrationsToStart.push(this);
2278
2295
  });
2296
+ if (isOutermostMount) {
2297
+ registrationsToStart.forEach(
2298
+ (store) => store.startReactiveRegistrations()
2299
+ );
2300
+ }
2279
2301
  } catch (error) {
2280
2302
  if (error instanceof Error && (error instanceof RangeError && /call stack/i.test(error.message) || /cycle detected/i.test(error.message))) {
2281
2303
  throw createCircularMountError(frame);
@@ -2307,9 +2329,14 @@ class StoreAdministration extends PreactObjectAdministration {
2307
2329
  this.contextCache.clear();
2308
2330
  this.parent = null;
2309
2331
  });
2310
- this.mountedSignal.set(false);
2311
2332
  this.disposed = true;
2312
- Array.from(this.ownedDisposers).forEach((dispose) => dispose());
2333
+ this.reactiveRegistrations.forEach((registration) => {
2334
+ registration.disposed = true;
2335
+ registration.stop?.();
2336
+ registration.stop = void 0;
2337
+ });
2338
+ this.reactiveRegistrations.clear();
2339
+ this.mountedSignal.set(false);
2313
2340
  }
2314
2341
  }
2315
2342
  let initEnabled$1 = false;
@@ -2035,7 +2035,7 @@ class StoreAdministration extends PreactObjectAdministration {
2035
2035
  disposed = false;
2036
2036
  contextCache = /* @__PURE__ */ new Map();
2037
2037
  childStoreDataMap = /* @__PURE__ */ new Map();
2038
- ownedDisposers = /* @__PURE__ */ new Set();
2038
+ reactiveRegistrations = /* @__PURE__ */ new Set();
2039
2039
  configurationGetter;
2040
2040
  setConfiguration(configurationGetter) {
2041
2041
  this.configurationGetter = configurationGetter;
@@ -2059,7 +2059,9 @@ class StoreAdministration extends PreactObjectAdministration {
2059
2059
  }
2060
2060
  });
2061
2061
  childStoreData.value.set(stores);
2062
- stores.forEach((s) => getStoreAdm(s).mount(this, name));
2062
+ if (this.isMounted) {
2063
+ stores.forEach((s) => getStoreAdm(s).mount(this, name));
2064
+ }
2063
2065
  return stores;
2064
2066
  }
2065
2067
  const newStores = /* @__PURE__ */ new Set();
@@ -2110,7 +2112,9 @@ class StoreAdministration extends PreactObjectAdministration {
2110
2112
  batch(() => childStoreData.value.set(stores));
2111
2113
  }
2112
2114
  removedStores.forEach((s) => getStoreAdm(s).dispose(true));
2113
- newStores.forEach((s) => getStoreAdm(s).mount(this, name));
2115
+ if (this.isMounted) {
2116
+ newStores.forEach((s) => getStoreAdm(s).mount(this, name));
2117
+ }
2114
2118
  return stores;
2115
2119
  }
2116
2120
  setSingleStore(name, element) {
@@ -2129,7 +2133,9 @@ class StoreAdministration extends PreactObjectAdministration {
2129
2133
  }
2130
2134
  const childStore = this.createChildStore(element);
2131
2135
  batch(() => childStoreData.value.set(childStore));
2132
- getStoreAdm(childStore).mount(this, name);
2136
+ if (this.isMounted) {
2137
+ getStoreAdm(childStore).mount(this, name);
2138
+ }
2133
2139
  return childStore;
2134
2140
  } else {
2135
2141
  batch(() => updateProps(oldStore.props, props));
@@ -2222,31 +2228,37 @@ class StoreAdministration extends PreactObjectAdministration {
2222
2228
  }
2223
2229
  return void 0;
2224
2230
  }
2225
- own(disposer) {
2231
+ register(start) {
2226
2232
  if (this.disposed) {
2227
- disposer();
2228
2233
  throw new Error(
2229
2234
  "r-state-tree: cannot register reactive resources on a disposed store"
2230
2235
  );
2231
2236
  }
2232
- let active = true;
2233
- const wrapped = () => {
2234
- if (!active) return;
2235
- active = false;
2236
- this.ownedDisposers.delete(wrapped);
2237
- disposer();
2237
+ const registration = { start, disposed: false };
2238
+ this.reactiveRegistrations.add(registration);
2239
+ if (this.isMounted) registration.stop = registration.start();
2240
+ return () => {
2241
+ if (registration.disposed) return;
2242
+ registration.disposed = true;
2243
+ this.reactiveRegistrations.delete(registration);
2244
+ registration.stop?.();
2245
+ registration.stop = void 0;
2238
2246
  };
2239
- this.ownedDisposers.add(wrapped);
2240
- return wrapped;
2247
+ }
2248
+ startReactiveRegistrations() {
2249
+ this.reactiveRegistrations.forEach((registration) => {
2250
+ if (!registration.disposed && !registration.stop) {
2251
+ registration.stop = registration.start();
2252
+ }
2253
+ });
2241
2254
  }
2242
2255
  reaction(track, callback) {
2243
- const unsub = reaction(track, callback);
2244
- return this.own(unsub);
2256
+ return this.register(() => reaction(track, callback));
2245
2257
  }
2246
2258
  effect(callback) {
2247
- return this.own(effect(callback));
2259
+ return this.register(() => effect(callback));
2248
2260
  }
2249
- mount(parent = null, childName) {
2261
+ mount(parent = null, childName, activationQueue) {
2250
2262
  if (this.disposed) {
2251
2263
  throw new Error("r-state-tree: cannot mount a disposed store");
2252
2264
  }
@@ -2262,19 +2274,29 @@ class StoreAdministration extends PreactObjectAdministration {
2262
2274
  throw createCircularMountError(frame);
2263
2275
  }
2264
2276
  mountingStack.push(frame);
2277
+ const isOutermostMount = activationQueue === void 0;
2278
+ const registrationsToStart = activationQueue ?? [];
2265
2279
  try {
2266
2280
  batch(() => {
2267
2281
  this.parent = parent || null;
2268
2282
  this.childStoreDataMap.forEach(({ value }, name) => {
2269
2283
  const stores = value.get();
2270
2284
  if (Array.isArray(stores)) {
2271
- stores?.forEach((s) => getStoreAdm(s)?.mount(this, name));
2285
+ stores?.forEach(
2286
+ (s) => getStoreAdm(s)?.mount(this, name, registrationsToStart)
2287
+ );
2272
2288
  } else if (stores) {
2273
- getStoreAdm(stores)?.mount(this, name);
2289
+ getStoreAdm(stores)?.mount(this, name, registrationsToStart);
2274
2290
  }
2275
2291
  });
2276
2292
  this.mountedSignal.set(true);
2293
+ registrationsToStart.push(this);
2277
2294
  });
2295
+ if (isOutermostMount) {
2296
+ registrationsToStart.forEach(
2297
+ (store) => store.startReactiveRegistrations()
2298
+ );
2299
+ }
2278
2300
  } catch (error) {
2279
2301
  if (error instanceof Error && (error instanceof RangeError && /call stack/i.test(error.message) || /cycle detected/i.test(error.message))) {
2280
2302
  throw createCircularMountError(frame);
@@ -2306,9 +2328,14 @@ class StoreAdministration extends PreactObjectAdministration {
2306
2328
  this.contextCache.clear();
2307
2329
  this.parent = null;
2308
2330
  });
2309
- this.mountedSignal.set(false);
2310
2331
  this.disposed = true;
2311
- Array.from(this.ownedDisposers).forEach((dispose) => dispose());
2332
+ this.reactiveRegistrations.forEach((registration) => {
2333
+ registration.disposed = true;
2334
+ registration.stop?.();
2335
+ registration.stop = void 0;
2336
+ });
2337
+ this.reactiveRegistrations.clear();
2338
+ this.mountedSignal.set(false);
2312
2339
  }
2313
2340
  }
2314
2341
  let initEnabled$1 = false;
@@ -10,7 +10,7 @@ export declare class StoreAdministration<StoreType extends Store = Store> extend
10
10
  private disposed;
11
11
  private contextCache;
12
12
  private childStoreDataMap;
13
- private ownedDisposers;
13
+ private reactiveRegistrations;
14
14
  private configurationGetter?;
15
15
  setConfiguration(configurationGetter: () => StoreConfiguration<StoreType>): void;
16
16
  private get configuration();
@@ -26,9 +26,10 @@ export declare class StoreAdministration<StoreType extends Store = Store> extend
26
26
  get isMounted(): boolean;
27
27
  getContextValue<T>(contextId: symbol, provideSymbol: symbol, defaultValue: T | undefined, hasDefault: boolean): T;
28
28
  private lookupContextValue;
29
- private own;
29
+ private register;
30
+ private startReactiveRegistrations;
30
31
  reaction<T>(track: () => T, callback: (value: T, previousValue: T) => void): () => void;
31
32
  effect(callback: () => void | (() => void)): () => void;
32
- mount(parent?: StoreAdministration | null, childName?: PropertyKey): void;
33
+ mount(parent?: StoreAdministration | null, childName?: PropertyKey, activationQueue?: StoreAdministration[]): void;
33
34
  dispose(internal?: boolean): void;
34
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "r-state-tree",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "reactive state management library",
5
5
  "main": "dist/r-state-tree.cjs",
6
6
  "module": "dist/r-state-tree.js",