rask-ui 0.10.4 → 0.11.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
@@ -484,9 +484,41 @@ function Timer() {
484
484
  }
485
485
  ```
486
486
 
487
+ **Effect with Disposal:**
488
+
489
+ The callback can optionally return a dispose function that runs before the effect executes again:
490
+
491
+ ```tsx
492
+ import { createEffect, createState } from "rask-ui";
493
+
494
+ function LiveData() {
495
+ const state = createState({ url: "/api/data", data: null });
496
+
497
+ createEffect(() => {
498
+ const eventSource = new EventSource(state.url);
499
+
500
+ eventSource.onmessage = (event) => {
501
+ state.data = JSON.parse(event.data);
502
+ };
503
+
504
+ // Dispose function runs before effect re-executes
505
+ return () => {
506
+ eventSource.close();
507
+ };
508
+ });
509
+
510
+ return () => (
511
+ <div>
512
+ <input value={state.url} onInput={(e) => state.url = e.target.value} />
513
+ <pre>{JSON.stringify(state.data, null, 2)}</pre>
514
+ </div>
515
+ );
516
+ }
517
+ ```
518
+
487
519
  **Parameters:**
488
520
 
489
- - `callback: () => void` - Function to run when dependencies change
521
+ - `callback: () => void | (() => void)` - Function to run when dependencies change. Can optionally return a dispose function that runs before the effect executes again.
490
522
 
491
523
  **Features:**
492
524
 
@@ -494,13 +526,16 @@ function Timer() {
494
526
  - Automatically tracks reactive dependencies accessed during execution
495
527
  - Re-runs on microtask when dependencies change (prevents synchronous cascades)
496
528
  - Automatically cleaned up when component unmounts
497
- - Can be used for side effects like logging, syncing to localStorage, or updating derived state
529
+ - Optional dispose function for cleaning up resources before re-execution
530
+ - Can be used for side effects like logging, syncing to localStorage, managing subscriptions, or updating derived state
498
531
 
499
532
  **Notes:**
500
533
 
501
534
  - Only call during component setup phase (not in render function)
502
535
  - Effects are queued on microtask to avoid synchronous execution from prop changes
503
536
  - Be careful with effects that modify state - can cause infinite loops if not careful
537
+ - Dispose functions run before the effect re-executes, not when the component unmounts
538
+ - For component unmount cleanup, use `createCleanup()` instead
504
539
 
505
540
  ---
506
541
 
@@ -1,2 +1,2 @@
1
- export declare function createEffect(cb: () => void): void;
1
+ export declare function createEffect(cb: () => void | (() => void)): void;
2
2
  //# sourceMappingURL=createEffect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createEffect.d.ts","sourceRoot":"","sources":["../src/createEffect.ts"],"names":[],"mappings":"AAGA,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,IAAI,QAwB1C"}
1
+ {"version":3,"file":"createEffect.d.ts","sourceRoot":"","sources":["../src/createEffect.ts"],"names":[],"mappings":"AAGA,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,QA6BzD"}
@@ -8,14 +8,21 @@ export function createEffect(cb) {
8
8
  catch {
9
9
  currentComponent = undefined;
10
10
  }
11
+ let disposer;
11
12
  const observer = new Observer(() => {
12
13
  // We trigger effects on micro task as synchronous observer notifications
13
14
  // (Like when components sets props) should not synchronously trigger effects
14
15
  queueMicrotask(runEffect);
15
16
  });
16
17
  const runEffect = () => {
18
+ try {
19
+ disposer?.();
20
+ }
21
+ catch (error) {
22
+ console.error("Error in effect dispose function:", error);
23
+ }
17
24
  const stopObserving = observer.observe();
18
- cb();
25
+ disposer = cb();
19
26
  stopObserving();
20
27
  };
21
28
  if (currentComponent) {
@@ -1,54 +1,32 @@
1
- type Simplify<T> = {
1
+ /**
2
+ * Utility type that preserves unions while "flattening" object types.
3
+ */
4
+ type Simplify<T> = T extends any ? {
2
5
  [K in keyof T]: T[K];
3
- } & {};
4
- type MergeTwo<A extends object, B extends object> = Simplify<Omit<A, keyof B> & B>;
6
+ } : never;
7
+ /**
8
+ * Merge two object types where properties from B override properties from A.
9
+ * Distributes over unions in A.
10
+ */
11
+ type MergeTwo<A extends object, B extends object> = A extends any ? Simplify<Omit<A, keyof B> & B> : never;
12
+ /**
13
+ * Merge a readonly tuple of objects left-to-right, with later entries
14
+ * overriding earlier ones. Distributes over unions in the head element.
15
+ */
5
16
  type MergeMany<T extends readonly object[]> = T extends [
6
17
  infer H extends object,
7
18
  ...infer R extends object[]
8
19
  ] ? MergeTwo<H, MergeMany<R>> : {};
9
20
  /**
10
- * Creates a view that merges multiple objects (reactive or not) into a single object while
11
- * maintaining reactivity through getters. Properties from later arguments override earlier ones.
12
- *
13
- * @warning **Do not destructure the returned view object!** Destructuring breaks reactivity
14
- * because it extracts plain values instead of maintaining getter access. This is the same rule
15
- * as other reactive primitives.
16
- *
17
- * @example
18
- * // ❌ Bad - destructuring loses reactivity
19
- * function Component() {
20
- * const state = createState({ count: 0 });
21
- * const helpers = { increment: () => state.count++ };
22
- * const view = createView(state, helpers);
23
- * const { count, increment } = view; // Don't do this!
24
- * return () => <button onClick={increment}>{count}</button>; // Won't update!
25
- * }
26
- *
27
- * // ✅ Good - access properties directly in render
28
- * function Component() {
29
- * const state = createState({ count: 0 });
30
- * const helpers = { increment: () => state.count++ };
31
- * const view = createView(state, helpers);
32
- * return () => <button onClick={view.increment}>{view.count}</button>; // Reactive!
33
- * }
34
- *
35
- * @example
36
- * // Merge multiple reactive objects
37
- * const state = createState({ count: 0 });
38
- * const user = createState({ name: "Alice" });
39
- * const view = createView(state, user);
40
- * // view has both count and name properties, maintaining reactivity
41
- *
42
- * @example
43
- * // Later arguments override earlier ones
44
- * const a = { x: 1, y: 2 };
45
- * const b = { y: 3, z: 4 };
46
- * const view = createView(a, b);
47
- * // view.x === 1, view.y === 3, view.z === 4
21
+ * Creates a view that merges multiple objects (reactive or not) into a single
22
+ * object while maintaining reactivity through getters. Properties from later
23
+ * arguments override earlier ones.
48
24
  *
49
- * @param args - Objects to merge (reactive or plain objects)
50
- * @returns A view object with getters for all properties, maintaining reactivity
25
+ * ⚠️ Do not destructure the returned view object; always read properties
26
+ * directly from the view to preserve reactivity.
51
27
  */
28
+ export declare function createView<A extends object>(a: A): A;
29
+ export declare function createView<A extends object, B extends object>(a: A, b: B): MergeTwo<A, B>;
52
30
  export declare function createView<T extends readonly object[]>(...args: T): MergeMany<T>;
53
31
  export {};
54
32
  //# sourceMappingURL=createView.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createView.d.ts","sourceRoot":"","sources":["../src/createView.ts"],"names":[],"mappings":"AAGA,KAAK,QAAQ,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAE,GAAG,EAAE,CAAC;AAEjD,KAAK,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,IAAI,QAAQ,CAC1D,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,CACrB,CAAC;AAEF,KAAK,SAAS,CAAC,CAAC,SAAS,SAAS,MAAM,EAAE,IAAI,CAAC,SAAS;IACtD,MAAM,CAAC,SAAS,MAAM;IACtB,GAAG,MAAM,CAAC,SAAS,MAAM,EAAE;CAC5B,GACG,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,GACzB,EAAE,CAAC;AAEP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,SAAS,MAAM,EAAE,EACpD,GAAG,IAAI,EAAE,CAAC,GACT,SAAS,CAAC,CAAC,CAAC,CAyEd"}
1
+ {"version":3,"file":"createView.d.ts","sourceRoot":"","sources":["../src/createView.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,KAAK,QAAQ,CAAC,CAAC,IAAI,CAAC,SAAS,GAAG,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAE,GAAG,KAAK,CAAC;AAEpE;;;GAGG;AACH,KAAK,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,IAAI,CAAC,SAAS,GAAG,GAC7D,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,GAC9B,KAAK,CAAC;AAEV;;;GAGG;AACH,KAAK,SAAS,CAAC,CAAC,SAAS,SAAS,MAAM,EAAE,IAAI,CAAC,SAAS;IACtD,MAAM,CAAC,SAAS,MAAM;IACtB,GAAG,MAAM,CAAC,SAAS,MAAM,EAAE;CAC5B,GACG,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,GACzB,EAAE,CAAC;AAEP;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;AACtD,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,EAC3D,CAAC,EAAE,CAAC,EACJ,CAAC,EAAE,CAAC,GACH,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAClB,wBAAgB,UAAU,CAAC,CAAC,SAAS,SAAS,MAAM,EAAE,EACpD,GAAG,IAAI,EAAE,CAAC,GACT,SAAS,CAAC,CAAC,CAAC,CAAC"}
@@ -1,64 +1,22 @@
1
1
  import { INSPECT_MARKER, INSPECTOR_ENABLED } from "./inspect";
2
- /**
3
- * Creates a view that merges multiple objects (reactive or not) into a single object while
4
- * maintaining reactivity through getters. Properties from later arguments override earlier ones.
5
- *
6
- * @warning **Do not destructure the returned view object!** Destructuring breaks reactivity
7
- * because it extracts plain values instead of maintaining getter access. This is the same rule
8
- * as other reactive primitives.
9
- *
10
- * @example
11
- * // ❌ Bad - destructuring loses reactivity
12
- * function Component() {
13
- * const state = createState({ count: 0 });
14
- * const helpers = { increment: () => state.count++ };
15
- * const view = createView(state, helpers);
16
- * const { count, increment } = view; // Don't do this!
17
- * return () => <button onClick={increment}>{count}</button>; // Won't update!
18
- * }
19
- *
20
- * // ✅ Good - access properties directly in render
21
- * function Component() {
22
- * const state = createState({ count: 0 });
23
- * const helpers = { increment: () => state.count++ };
24
- * const view = createView(state, helpers);
25
- * return () => <button onClick={view.increment}>{view.count}</button>; // Reactive!
26
- * }
27
- *
28
- * @example
29
- * // Merge multiple reactive objects
30
- * const state = createState({ count: 0 });
31
- * const user = createState({ name: "Alice" });
32
- * const view = createView(state, user);
33
- * // view has both count and name properties, maintaining reactivity
34
- *
35
- * @example
36
- * // Later arguments override earlier ones
37
- * const a = { x: 1, y: 2 };
38
- * const b = { y: 3, z: 4 };
39
- * const view = createView(a, b);
40
- * // view.x === 1, view.y === 3, view.z === 4
41
- *
42
- * @param args - Objects to merge (reactive or plain objects)
43
- * @returns A view object with getters for all properties, maintaining reactivity
44
- */
45
2
  export function createView(...args) {
46
3
  const result = {};
47
4
  const seen = new Set();
48
5
  let notifyInspectorRef = {};
49
6
  for (let i = args.length - 1; i >= 0; i--) {
50
7
  const src = args[i];
8
+ if (!src)
9
+ continue;
51
10
  if (INSPECTOR_ENABLED && src[INSPECT_MARKER]) {
52
11
  src[INSPECT_MARKER] = notifyInspectorRef;
53
12
  }
54
- // mimic Object.assign: only enumerable own property keys
13
+ // Mimic Object.assign: only enumerable own property keys
55
14
  for (const key of Reflect.ownKeys(src)) {
56
15
  if (seen.has(key))
57
16
  continue;
58
17
  const desc = Object.getOwnPropertyDescriptor(src, key);
59
18
  if (!desc || !desc.enumerable)
60
19
  continue;
61
- // Capture the current source for this key (last write wins).
62
20
  Object.defineProperty(result, key, {
63
21
  enumerable: true,
64
22
  configurable: true,
@@ -67,6 +25,7 @@ export function createView(...args) {
67
25
  if (!INSPECTOR_ENABLED || !notifyInspectorRef.current) {
68
26
  return value;
69
27
  }
28
+ // Propagate inspector marker into nested observables
70
29
  if (value?.[INSPECT_MARKER]) {
71
30
  value[INSPECT_MARKER] = {
72
31
  current: {
@@ -76,6 +35,7 @@ export function createView(...args) {
76
35
  };
77
36
  }
78
37
  else if (typeof value === "function") {
38
+ // Wrap actions to notify inspector
79
39
  return (...params) => {
80
40
  notifyInspectorRef.current.notify({
81
41
  type: "action",
@@ -100,6 +60,7 @@ export function createView(...args) {
100
60
  },
101
61
  set: (value) => {
102
62
  Object.defineProperty(notifyInspectorRef, "current", {
63
+ configurable: true,
103
64
  get() {
104
65
  return value.current;
105
66
  },
@@ -107,5 +68,6 @@ export function createView(...args) {
107
68
  },
108
69
  });
109
70
  }
71
+ // The overload signatures expose a precise type; this is the shared impl.
110
72
  return result;
111
73
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rask-ui",
3
- "version": "0.10.4",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",