@virentia/react 0.1.0 → 0.2.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/README.md CHANGED
@@ -75,6 +75,23 @@ export const Counter = component({
75
75
  });
76
76
  ```
77
77
 
78
+ `component` also exposes `.create()` and accepts a `model` prop for controlled usage.
79
+ The created model owns its scope and should be disposed by the caller.
80
+
81
+ ```tsx
82
+ const Parent = component({
83
+ model() {
84
+ const counter = Counter.create({ step: 2 });
85
+
86
+ return { counter };
87
+ },
88
+
89
+ view({ model }) {
90
+ return <Counter step={2} model={model.counter} />;
91
+ },
92
+ });
93
+ ```
94
+
78
95
  ## Model Caches
79
96
 
80
97
  Use `createModelCache` when a model should survive unmount and be reused by key: chats, tabs, detail screens, media players, or previews.
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- let react = require("react");
3
2
  let _virentia_core = require("@virentia/core");
3
+ let react = require("react");
4
4
  //#region lib/model-cache.ts
5
5
  function createModelCache() {
6
6
  const byScope = /* @__PURE__ */ new WeakMap();
@@ -91,6 +91,9 @@ function useProvidedScope() {
91
91
  if (!scope) throw new Error("[useProvidedScope] Scope is not provided. Wrap your tree with ScopeProvider.");
92
92
  return scope;
93
93
  }
94
+ function useOptionalProvidedScope() {
95
+ return (0, react.useContext)(ScopeContext);
96
+ }
94
97
  //#endregion
95
98
  //#region lib/utils.ts
96
99
  const useIsomorphicLayoutEffect = typeof window === "undefined" ? react.useEffect : react.useLayoutEffect;
@@ -177,7 +180,8 @@ function useStoreUnit(unit, scope) {
177
180
  function useModel(modelOrFactory, props, options) {
178
181
  const scope = useProvidedScope();
179
182
  if (typeof modelOrFactory !== "function") return useReactiveModel(modelOrFactory, scope);
180
- return useReactiveModel(useModelInstance(modelOrFactory, props, scope, options).model, scope);
183
+ const instance = useModelInstance(modelOrFactory, props, scope, options);
184
+ return useReactiveModel(instance.model, instance.scope);
181
185
  }
182
186
  function useModelInstance(factory, props, scope, options) {
183
187
  const cache = options?.cache;
@@ -191,6 +195,11 @@ function useModelInstance(factory, props, scope, options) {
191
195
  key,
192
196
  scope
193
197
  ]);
198
+ useModelInstanceLifecycle(instance, props, { disposeOnUnmount: !cached });
199
+ return instance;
200
+ }
201
+ function useModelInstanceLifecycle(instance, props, options) {
202
+ const scope = instance.scope;
194
203
  useIsomorphicLayoutEffect(() => {
195
204
  writeStore(instance.props, props, scope);
196
205
  }, [
@@ -208,14 +217,13 @@ function useModelInstance(factory, props, scope, options) {
208
217
  instance.mounts.value = Math.max(0, instance.mounts.value - 1);
209
218
  instance.unmounted();
210
219
  });
211
- if (!cached) instance.dispose();
220
+ if (options.disposeOnUnmount) instance.dispose();
212
221
  };
213
222
  }, [
214
- cached,
215
223
  instance,
224
+ options.disposeOnUnmount,
216
225
  scope
217
226
  ]);
218
- return instance;
219
227
  }
220
228
  function createModelInstance(factory, props, scope, key) {
221
229
  return (0, _virentia_core.owner)((dispose, modelOwner) => {
@@ -236,34 +244,83 @@ function createModelInstance(factory, props, scope, key) {
236
244
  };
237
245
  });
238
246
  }
247
+ function exposeModelInstance(instance) {
248
+ const model = instance.model;
249
+ Object.defineProperty(model, modelInstanceSymbol, {
250
+ configurable: true,
251
+ value: instance
252
+ });
253
+ defineHidden(model, "dispose", () => instance.dispose());
254
+ defineHidden(model, disposeSymbol, () => instance.dispose());
255
+ return model;
256
+ }
257
+ function readExposedModelInstance(model) {
258
+ return model[modelInstanceSymbol] ?? null;
259
+ }
239
260
  function useReactiveModel(model, scope) {
240
261
  const result = {};
241
262
  for (const key of Reflect.ownKeys(model)) {
242
- if (key === "dispose" || key === disposeSymbol) continue;
263
+ const descriptor = Reflect.getOwnPropertyDescriptor(model, key);
264
+ if (key === "dispose" || key === disposeSymbol || descriptor && !descriptor.enumerable) continue;
243
265
  result[key] = useModelValue(Reflect.get(model, key), scope);
244
266
  }
245
267
  return result;
246
268
  }
247
269
  const disposeSymbol = typeof Symbol.dispose === "symbol" ? Symbol.dispose : Symbol.for("Symbol.dispose");
270
+ const modelInstanceSymbol = Symbol("virentia.react.modelInstance");
271
+ function defineHidden(target, key, value) {
272
+ if (key in target) return;
273
+ Object.defineProperty(target, key, {
274
+ configurable: true,
275
+ value
276
+ });
277
+ }
248
278
  function useModelValue(value, scope) {
249
279
  if (isUnitLike(value)) return useUnitWithScope(value, scope);
280
+ if (isComponentModel(value)) return value;
250
281
  if (isPlainObject(value)) return useReactiveModel(value, scope);
251
282
  return value;
252
283
  }
284
+ function isComponentModel(value) {
285
+ return Boolean(value && typeof value === "object" && value[modelInstanceSymbol]);
286
+ }
253
287
  //#endregion
254
288
  //#region lib/component.ts
255
289
  function component(config) {
256
290
  const VirentiaComponent = (props) => {
257
- const model = "cache" in config ? useModel(config.model, props, {
258
- cache: config.cache,
259
- key: config.key(props)
260
- }) : useModel(config.model, props);
291
+ const { model: controlledModel, ...modelProps } = props;
292
+ const providedScope = useOptionalProvidedScope();
293
+ const controlledInstance = controlledModel ? readExposedModelInstance(controlledModel) : null;
294
+ const key = "cache" in config ? config.key(modelProps) : void 0;
295
+ const instance = (0, react.useMemo)(() => {
296
+ if (controlledModel) {
297
+ if (!controlledInstance) throw new Error("[component] The model prop must be created with component.create().");
298
+ return controlledInstance;
299
+ }
300
+ if (!providedScope) throw new Error("[useProvidedScope] Scope is not provided. Wrap your tree with ScopeProvider.");
301
+ if ("cache" in config) return getOrCreateCachedInstance(config.cache, providedScope, key, () => createModelInstance(config.model, modelProps, providedScope, key));
302
+ return createModelInstance(config.model, modelProps, providedScope, void 0);
303
+ }, [
304
+ controlledInstance,
305
+ controlledModel,
306
+ key,
307
+ providedScope
308
+ ]);
309
+ const cached = !controlledModel && "cache" in config;
310
+ const model = useReactiveModel(instance.model, instance.scope);
311
+ useModelInstanceLifecycle(instance, modelProps, { disposeOnUnmount: !controlledModel && !cached });
261
312
  return (0, react.createElement)(config.view, {
262
- ...props,
313
+ ...modelProps,
263
314
  model
264
315
  });
265
316
  };
266
317
  VirentiaComponent.displayName = getComponentName(config.view);
318
+ VirentiaComponent.create = ((props) => {
319
+ const externalScope = (0, _virentia_core.getCurrentScope)();
320
+ if (!externalScope) throw new Error("[component.create] Parent component context is required. Call .create() while creating a parent component model.");
321
+ const key = "cache" in config ? config.key(props) : void 0;
322
+ return exposeModelInstance(createModelInstance(config.model, props, externalScope, key));
323
+ });
267
324
  return VirentiaComponent;
268
325
  }
269
326
  //#endregion
package/dist/index.d.cts CHANGED
@@ -1,10 +1,11 @@
1
+ import { DisposableOwner, Effect, EffectCallArgs, EventCallable, EventPayload, Owner, Scope, Store, StoreWritable } from "@virentia/core";
1
2
  import { ComponentType, FC, ReactNode } from "react";
2
- import { Effect, EffectCallArgs, EventCallable, EventPayload, Owner, Scope, Store, StoreWritable } from "@virentia/core";
3
3
 
4
4
  //#region lib/types.d.ts
5
5
  type UnitLike = Store<any> | StoreWritable<any> | EventCallable<any> | Effect<any, any, any>;
6
+ declare const componentModelBrand: unique symbol;
6
7
  type UnitValue<Unit> = Unit extends Store<infer State> | StoreWritable<infer State> ? State : Unit extends EventCallable<infer Payload> ? (...payload: EventPayload<Payload>) => Promise<void> : Unit extends Effect<infer Params, infer Done, any> ? (...args: EffectCallArgs<Params>) => Promise<Done> : never;
7
- type ReactiveModel<Model> = { readonly [Key in keyof Model as Key extends "dispose" ? never : Key]: Model[Key] extends UnitLike ? UnitValue<Model[Key]> : Model[Key] extends ((...args: any[]) => any) ? Model[Key] : Model[Key] extends object ? ReactiveModel<Model[Key]> : Model[Key] };
8
+ type ReactiveModel<Model> = { readonly [Key in keyof Model as Key extends "dispose" ? never : Key]: Model[Key] extends ComponentModel<infer ChildModel> ? ComponentModel<ChildModel> : Model[Key] extends UnitLike ? UnitValue<Model[Key]> : Model[Key] extends ((...args: any[]) => any) ? Model[Key] : Model[Key] extends object ? ReactiveModel<Model[Key]> : Model[Key] };
8
9
  interface ModelContext<Props, Key = undefined> {
9
10
  readonly scope: Scope;
10
11
  readonly owner: Owner;
@@ -26,11 +27,23 @@ interface ModelCache<Key, Props, Model extends object> {
26
27
  delete(key: Key, scope?: Scope): boolean;
27
28
  clear(scope?: Scope): void;
28
29
  }
29
- type UnitShape<Shape> = Shape extends readonly UnitLike[] ? { readonly [Key in keyof Shape]: UnitValue<Shape[Key]> } : Shape extends Record<string, UnitLike> ? { readonly [Key in keyof Shape]: UnitValue<Shape[Key]> } : never;
30
+ type UnitShape<Shape> = Shape extends readonly unknown[] ? { readonly [Key in keyof Shape]: UnitValue<Shape[Key]> } : Shape extends Record<string, unknown> ? { readonly [Key in keyof Shape]: UnitValue<Shape[Key]> } : never;
30
31
  type CacheOptions<Props, Key, Model extends object> = {
31
32
  readonly cache: ModelCache<Key, Props, Model>;
32
33
  readonly key: Key;
33
34
  };
35
+ type ComponentModel<Model extends object> = Model & DisposableOwner & {
36
+ readonly [componentModelBrand]: true;
37
+ };
38
+ type ComponentPublicProps<Props, Model extends object> = Omit<Props, "model"> & {
39
+ readonly model?: ComponentModel<Model>;
40
+ };
41
+ interface ComponentCreate<Props, Model extends object> {
42
+ (props: Props): ComponentModel<Model>;
43
+ }
44
+ type VirentiaComponent<Props, Model extends object> = FC<ComponentPublicProps<Props, Model>> & {
45
+ readonly create: ComponentCreate<Props, Model>;
46
+ };
34
47
  type ComponentConfig<Props, Model extends object> = {
35
48
  readonly model: ModelFactory<Props, Model>;
36
49
  readonly view: ComponentType<Props & {
@@ -47,8 +60,8 @@ type CachedComponentConfig<Props, Key, Model extends object> = {
47
60
  };
48
61
  //#endregion
49
62
  //#region lib/component.d.ts
50
- declare function component<Props, Model extends object>(config: ComponentConfig<Props, Model>): FC<Props>;
51
- declare function component<Props, Key, Model extends object>(config: CachedComponentConfig<Props, Key, Model>): FC<Props>;
63
+ declare function component<Props, Model extends object>(config: ComponentConfig<Props, Model>): VirentiaComponent<Props, Model>;
64
+ declare function component<Props, Key, Model extends object>(config: CachedComponentConfig<Props, Key, Model>): VirentiaComponent<Props, Model>;
52
65
  //#endregion
53
66
  //#region lib/model-cache.d.ts
54
67
  declare function createModelCache<Key, Props, Model extends object>(): ModelCache<Key, Props, Model>;
@@ -66,8 +79,11 @@ declare function useModel<Props, Model extends object>(factory: ModelFactory<Pro
66
79
  declare function useModel<Props, Key, Model extends object>(factory: ModelFactory<Props, Model, Key>, props: Props, options: CacheOptions<Props, Key, Model>): ReactiveModel<Model>;
67
80
  //#endregion
68
81
  //#region lib/use-unit.d.ts
69
- declare function useUnit<Unit extends UnitLike>(unit: Unit): UnitValue<Unit>;
70
- declare function useUnit<Shape extends readonly UnitLike[]>(shape: Shape): UnitShape<Shape>;
71
- declare function useUnit<Shape extends Record<string, UnitLike>>(shape: Shape): UnitShape<Shape>;
82
+ declare function useUnit<State>(unit: StoreWritable<State>): State;
83
+ declare function useUnit<State>(unit: Store<State>): State;
84
+ declare function useUnit<Payload>(unit: EventCallable<Payload>): UnitValue<EventCallable<Payload>>;
85
+ declare function useUnit<Params, Done, Fail>(unit: Effect<Params, Done, Fail>): UnitValue<Effect<Params, Done, Fail>>;
86
+ declare function useUnit<const Shape extends readonly UnitLike[]>(shape: Shape): UnitShape<Shape>;
87
+ declare function useUnit<const Shape extends Record<string, UnitLike>>(shape: Shape): UnitShape<Shape>;
72
88
  //#endregion
73
- export { type ModelCache, type ModelContext, type ModelFactory, type ModelInstance, type ReactiveModel, ScopeProvider, type UnitLike, type UnitValue, component, createModelCache, useModel, useProvidedScope, useUnit };
89
+ export { type ComponentCreate, type ComponentModel, type ComponentPublicProps, type ModelCache, type ModelContext, type ModelFactory, type ModelInstance, type ReactiveModel, ScopeProvider, type UnitLike, type UnitValue, type VirentiaComponent, component, createModelCache, useModel, useProvidedScope, useUnit };
package/dist/index.d.mts CHANGED
@@ -1,10 +1,11 @@
1
+ import { DisposableOwner, Effect, EffectCallArgs, EventCallable, EventPayload, Owner, Scope, Store, StoreWritable } from "@virentia/core";
1
2
  import { ComponentType, FC, ReactNode } from "react";
2
- import { Effect, EffectCallArgs, EventCallable, EventPayload, Owner, Scope, Store, StoreWritable } from "@virentia/core";
3
3
 
4
4
  //#region lib/types.d.ts
5
5
  type UnitLike = Store<any> | StoreWritable<any> | EventCallable<any> | Effect<any, any, any>;
6
+ declare const componentModelBrand: unique symbol;
6
7
  type UnitValue<Unit> = Unit extends Store<infer State> | StoreWritable<infer State> ? State : Unit extends EventCallable<infer Payload> ? (...payload: EventPayload<Payload>) => Promise<void> : Unit extends Effect<infer Params, infer Done, any> ? (...args: EffectCallArgs<Params>) => Promise<Done> : never;
7
- type ReactiveModel<Model> = { readonly [Key in keyof Model as Key extends "dispose" ? never : Key]: Model[Key] extends UnitLike ? UnitValue<Model[Key]> : Model[Key] extends ((...args: any[]) => any) ? Model[Key] : Model[Key] extends object ? ReactiveModel<Model[Key]> : Model[Key] };
8
+ type ReactiveModel<Model> = { readonly [Key in keyof Model as Key extends "dispose" ? never : Key]: Model[Key] extends ComponentModel<infer ChildModel> ? ComponentModel<ChildModel> : Model[Key] extends UnitLike ? UnitValue<Model[Key]> : Model[Key] extends ((...args: any[]) => any) ? Model[Key] : Model[Key] extends object ? ReactiveModel<Model[Key]> : Model[Key] };
8
9
  interface ModelContext<Props, Key = undefined> {
9
10
  readonly scope: Scope;
10
11
  readonly owner: Owner;
@@ -26,11 +27,23 @@ interface ModelCache<Key, Props, Model extends object> {
26
27
  delete(key: Key, scope?: Scope): boolean;
27
28
  clear(scope?: Scope): void;
28
29
  }
29
- type UnitShape<Shape> = Shape extends readonly UnitLike[] ? { readonly [Key in keyof Shape]: UnitValue<Shape[Key]> } : Shape extends Record<string, UnitLike> ? { readonly [Key in keyof Shape]: UnitValue<Shape[Key]> } : never;
30
+ type UnitShape<Shape> = Shape extends readonly unknown[] ? { readonly [Key in keyof Shape]: UnitValue<Shape[Key]> } : Shape extends Record<string, unknown> ? { readonly [Key in keyof Shape]: UnitValue<Shape[Key]> } : never;
30
31
  type CacheOptions<Props, Key, Model extends object> = {
31
32
  readonly cache: ModelCache<Key, Props, Model>;
32
33
  readonly key: Key;
33
34
  };
35
+ type ComponentModel<Model extends object> = Model & DisposableOwner & {
36
+ readonly [componentModelBrand]: true;
37
+ };
38
+ type ComponentPublicProps<Props, Model extends object> = Omit<Props, "model"> & {
39
+ readonly model?: ComponentModel<Model>;
40
+ };
41
+ interface ComponentCreate<Props, Model extends object> {
42
+ (props: Props): ComponentModel<Model>;
43
+ }
44
+ type VirentiaComponent<Props, Model extends object> = FC<ComponentPublicProps<Props, Model>> & {
45
+ readonly create: ComponentCreate<Props, Model>;
46
+ };
34
47
  type ComponentConfig<Props, Model extends object> = {
35
48
  readonly model: ModelFactory<Props, Model>;
36
49
  readonly view: ComponentType<Props & {
@@ -47,8 +60,8 @@ type CachedComponentConfig<Props, Key, Model extends object> = {
47
60
  };
48
61
  //#endregion
49
62
  //#region lib/component.d.ts
50
- declare function component<Props, Model extends object>(config: ComponentConfig<Props, Model>): FC<Props>;
51
- declare function component<Props, Key, Model extends object>(config: CachedComponentConfig<Props, Key, Model>): FC<Props>;
63
+ declare function component<Props, Model extends object>(config: ComponentConfig<Props, Model>): VirentiaComponent<Props, Model>;
64
+ declare function component<Props, Key, Model extends object>(config: CachedComponentConfig<Props, Key, Model>): VirentiaComponent<Props, Model>;
52
65
  //#endregion
53
66
  //#region lib/model-cache.d.ts
54
67
  declare function createModelCache<Key, Props, Model extends object>(): ModelCache<Key, Props, Model>;
@@ -66,8 +79,11 @@ declare function useModel<Props, Model extends object>(factory: ModelFactory<Pro
66
79
  declare function useModel<Props, Key, Model extends object>(factory: ModelFactory<Props, Model, Key>, props: Props, options: CacheOptions<Props, Key, Model>): ReactiveModel<Model>;
67
80
  //#endregion
68
81
  //#region lib/use-unit.d.ts
69
- declare function useUnit<Unit extends UnitLike>(unit: Unit): UnitValue<Unit>;
70
- declare function useUnit<Shape extends readonly UnitLike[]>(shape: Shape): UnitShape<Shape>;
71
- declare function useUnit<Shape extends Record<string, UnitLike>>(shape: Shape): UnitShape<Shape>;
82
+ declare function useUnit<State>(unit: StoreWritable<State>): State;
83
+ declare function useUnit<State>(unit: Store<State>): State;
84
+ declare function useUnit<Payload>(unit: EventCallable<Payload>): UnitValue<EventCallable<Payload>>;
85
+ declare function useUnit<Params, Done, Fail>(unit: Effect<Params, Done, Fail>): UnitValue<Effect<Params, Done, Fail>>;
86
+ declare function useUnit<const Shape extends readonly UnitLike[]>(shape: Shape): UnitShape<Shape>;
87
+ declare function useUnit<const Shape extends Record<string, UnitLike>>(shape: Shape): UnitShape<Shape>;
72
88
  //#endregion
73
- export { type ModelCache, type ModelContext, type ModelFactory, type ModelInstance, type ReactiveModel, ScopeProvider, type UnitLike, type UnitValue, component, createModelCache, useModel, useProvidedScope, useUnit };
89
+ export { type ComponentCreate, type ComponentModel, type ComponentPublicProps, type ModelCache, type ModelContext, type ModelFactory, type ModelInstance, type ReactiveModel, ScopeProvider, type UnitLike, type UnitValue, type VirentiaComponent, component, createModelCache, useModel, useProvidedScope, useUnit };
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
+ import { event, getCurrentScope, owner, run, scoped, store } from "@virentia/core";
1
2
  import { createContext, createElement, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useSyncExternalStore } from "react";
2
- import { event, owner, run, scoped, store } from "@virentia/core";
3
3
  //#region lib/model-cache.ts
4
4
  function createModelCache() {
5
5
  const byScope = /* @__PURE__ */ new WeakMap();
@@ -90,6 +90,9 @@ function useProvidedScope() {
90
90
  if (!scope) throw new Error("[useProvidedScope] Scope is not provided. Wrap your tree with ScopeProvider.");
91
91
  return scope;
92
92
  }
93
+ function useOptionalProvidedScope() {
94
+ return useContext(ScopeContext);
95
+ }
93
96
  //#endregion
94
97
  //#region lib/utils.ts
95
98
  const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
@@ -176,7 +179,8 @@ function useStoreUnit(unit, scope) {
176
179
  function useModel(modelOrFactory, props, options) {
177
180
  const scope = useProvidedScope();
178
181
  if (typeof modelOrFactory !== "function") return useReactiveModel(modelOrFactory, scope);
179
- return useReactiveModel(useModelInstance(modelOrFactory, props, scope, options).model, scope);
182
+ const instance = useModelInstance(modelOrFactory, props, scope, options);
183
+ return useReactiveModel(instance.model, instance.scope);
180
184
  }
181
185
  function useModelInstance(factory, props, scope, options) {
182
186
  const cache = options?.cache;
@@ -190,6 +194,11 @@ function useModelInstance(factory, props, scope, options) {
190
194
  key,
191
195
  scope
192
196
  ]);
197
+ useModelInstanceLifecycle(instance, props, { disposeOnUnmount: !cached });
198
+ return instance;
199
+ }
200
+ function useModelInstanceLifecycle(instance, props, options) {
201
+ const scope = instance.scope;
193
202
  useIsomorphicLayoutEffect(() => {
194
203
  writeStore(instance.props, props, scope);
195
204
  }, [
@@ -207,14 +216,13 @@ function useModelInstance(factory, props, scope, options) {
207
216
  instance.mounts.value = Math.max(0, instance.mounts.value - 1);
208
217
  instance.unmounted();
209
218
  });
210
- if (!cached) instance.dispose();
219
+ if (options.disposeOnUnmount) instance.dispose();
211
220
  };
212
221
  }, [
213
- cached,
214
222
  instance,
223
+ options.disposeOnUnmount,
215
224
  scope
216
225
  ]);
217
- return instance;
218
226
  }
219
227
  function createModelInstance(factory, props, scope, key) {
220
228
  return owner((dispose, modelOwner) => {
@@ -235,34 +243,83 @@ function createModelInstance(factory, props, scope, key) {
235
243
  };
236
244
  });
237
245
  }
246
+ function exposeModelInstance(instance) {
247
+ const model = instance.model;
248
+ Object.defineProperty(model, modelInstanceSymbol, {
249
+ configurable: true,
250
+ value: instance
251
+ });
252
+ defineHidden(model, "dispose", () => instance.dispose());
253
+ defineHidden(model, disposeSymbol, () => instance.dispose());
254
+ return model;
255
+ }
256
+ function readExposedModelInstance(model) {
257
+ return model[modelInstanceSymbol] ?? null;
258
+ }
238
259
  function useReactiveModel(model, scope) {
239
260
  const result = {};
240
261
  for (const key of Reflect.ownKeys(model)) {
241
- if (key === "dispose" || key === disposeSymbol) continue;
262
+ const descriptor = Reflect.getOwnPropertyDescriptor(model, key);
263
+ if (key === "dispose" || key === disposeSymbol || descriptor && !descriptor.enumerable) continue;
242
264
  result[key] = useModelValue(Reflect.get(model, key), scope);
243
265
  }
244
266
  return result;
245
267
  }
246
268
  const disposeSymbol = typeof Symbol.dispose === "symbol" ? Symbol.dispose : Symbol.for("Symbol.dispose");
269
+ const modelInstanceSymbol = Symbol("virentia.react.modelInstance");
270
+ function defineHidden(target, key, value) {
271
+ if (key in target) return;
272
+ Object.defineProperty(target, key, {
273
+ configurable: true,
274
+ value
275
+ });
276
+ }
247
277
  function useModelValue(value, scope) {
248
278
  if (isUnitLike(value)) return useUnitWithScope(value, scope);
279
+ if (isComponentModel(value)) return value;
249
280
  if (isPlainObject(value)) return useReactiveModel(value, scope);
250
281
  return value;
251
282
  }
283
+ function isComponentModel(value) {
284
+ return Boolean(value && typeof value === "object" && value[modelInstanceSymbol]);
285
+ }
252
286
  //#endregion
253
287
  //#region lib/component.ts
254
288
  function component(config) {
255
289
  const VirentiaComponent = (props) => {
256
- const model = "cache" in config ? useModel(config.model, props, {
257
- cache: config.cache,
258
- key: config.key(props)
259
- }) : useModel(config.model, props);
290
+ const { model: controlledModel, ...modelProps } = props;
291
+ const providedScope = useOptionalProvidedScope();
292
+ const controlledInstance = controlledModel ? readExposedModelInstance(controlledModel) : null;
293
+ const key = "cache" in config ? config.key(modelProps) : void 0;
294
+ const instance = useMemo(() => {
295
+ if (controlledModel) {
296
+ if (!controlledInstance) throw new Error("[component] The model prop must be created with component.create().");
297
+ return controlledInstance;
298
+ }
299
+ if (!providedScope) throw new Error("[useProvidedScope] Scope is not provided. Wrap your tree with ScopeProvider.");
300
+ if ("cache" in config) return getOrCreateCachedInstance(config.cache, providedScope, key, () => createModelInstance(config.model, modelProps, providedScope, key));
301
+ return createModelInstance(config.model, modelProps, providedScope, void 0);
302
+ }, [
303
+ controlledInstance,
304
+ controlledModel,
305
+ key,
306
+ providedScope
307
+ ]);
308
+ const cached = !controlledModel && "cache" in config;
309
+ const model = useReactiveModel(instance.model, instance.scope);
310
+ useModelInstanceLifecycle(instance, modelProps, { disposeOnUnmount: !controlledModel && !cached });
260
311
  return createElement(config.view, {
261
- ...props,
312
+ ...modelProps,
262
313
  model
263
314
  });
264
315
  };
265
316
  VirentiaComponent.displayName = getComponentName(config.view);
317
+ VirentiaComponent.create = ((props) => {
318
+ const externalScope = getCurrentScope();
319
+ if (!externalScope) throw new Error("[component.create] Parent component context is required. Call .create() while creating a parent component model.");
320
+ const key = "cache" in config ? config.key(props) : void 0;
321
+ return exposeModelInstance(createModelInstance(config.model, props, externalScope, key));
322
+ });
266
323
  return VirentiaComponent;
267
324
  }
268
325
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@virentia/react",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "homepage": "https://movpushmov.dev/virentia/react/",
@@ -32,16 +32,21 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@virentia/core": "0.1.0"
35
+ "@virentia/core": "0.2.0"
36
36
  },
37
37
  "devDependencies": {
38
+ "@testing-library/react": "^16.3.0",
38
39
  "@types/react": "^19.2.7",
39
- "react": "^19.2.1"
40
+ "happy-dom": "^20.0.10",
41
+ "react": "^19.2.1",
42
+ "react-dom": "^19.2.1"
40
43
  },
41
44
  "peerDependencies": {
42
45
  "react": ">=18"
43
46
  },
44
47
  "scripts": {
45
- "build": "tsdown"
48
+ "build": "tsdown",
49
+ "test": "vitest run",
50
+ "test:watch": "vitest"
46
51
  }
47
52
  }