chem-rx 0.2.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 - 2026-06-14
4
+
5
+ ### Added
6
+
7
+ - `useSelector(atom, selector, equals?)` React hook (`chem-rx/react`): derive any
8
+ value from an atom with a selector function and re-render only when the selected
9
+ value changes. The optional `equals` comparator (defaults to `Object.is`) dedupes
10
+ re-renders by content, so an atom that churns references on every update only
11
+ re-renders the component when the selected slice is not equal. See
12
+ [useSelector](./README.md#useselector).
13
+
14
+ ### Removed (breaking)
15
+
16
+ - Removed `useSelectAtom`. Migrate to `useSelector` with a selector function:
17
+ `useSelectAtom(atom, 'key')` becomes `useSelector(atom, (s) => s.key)`.
18
+
3
19
  ## 0.2.0 - 2026-06-13
4
20
 
5
21
  ### Added
package/README.md CHANGED
@@ -389,25 +389,44 @@ function Counter() {
389
389
 
390
390
  Remember that you can mix and match for any of your needs
391
391
 
392
- ### useSelectAtom
392
+ ### useSelector
393
393
 
394
- With `useSelectAtom` you can select a specific key from an atom, and still have it live
395
- update in your react component.
394
+ With `useSelector` you can derive any value from an atom with a selector function, and
395
+ have your component live update re-rendering only when the selected value changes.
396
396
 
397
397
  ```
398
398
  import { Atom } from 'chem-rx'
399
- import { useSelectAtom } from 'chem-rx/react'
399
+ import { useSelector } from 'chem-rx/react'
400
400
 
401
401
  const count$ = Atom({ inner: 0 })
402
402
 
403
403
  function Counter() {
404
- const count = useSelectAtom(count$, 'inner')
404
+ const count = useSelector(count$, (s) => s.inner)
405
405
  return (
406
406
  <h1>
407
407
  {count}
408
408
  <button onClick={() => count$.set('inner', count + 2)}>one up</button> ...
409
409
  ```
410
410
 
411
+ `useSelector` takes an optional `equals` comparator (defaulting to `Object.is`) so you
412
+ can dedupe re-renders by content. This is the React-side counterpart to the atom-level
413
+ `equals` option described in [Equality & change detection](#equality--change-detection):
414
+ even if the parent atom rebuilds its value (and references) on every update, the
415
+ component only re-renders when the selected slice is *not* equal.
416
+
417
+ ```
418
+ const frame$ = Atom(initialFrame)
419
+
420
+ const itemsEqual = (a, b) =>
421
+ a.length === b.length && a.every((v, i) => v === b[i])
422
+
423
+ function ItemList() {
424
+ // re-renders only when `items` changes by content, not every frame
425
+ const items = useSelector(frame$, (f) => f.items, itemsEqual)
426
+ return <ul>{items.map((it) => <li key={it.id}>{it.label}</li>)}</ul>
427
+ }
428
+ ```
429
+
411
430
  ### hydrateAtoms
412
431
 
413
432
  With SSR, your atoms will likely need to be properly hydrated to prevent
package/dist/react.cjs.js CHANGED
@@ -38,13 +38,57 @@ function useSignal(signal, callback, id) {
38
38
  }, [signal, callback, id]);
39
39
  }
40
40
 
41
- function useSelectAtom(atom, key) {
42
- var selectedAtom = react.useMemo(function () {
43
- return atom.select(key);
44
- }, [atom, key]);
45
- return useAtom(selectedAtom);
41
+ function createSelectorMemo(selector, equals) {
42
+ var hasValue = false;
43
+ var prevSnapshot;
44
+ var prevSelection;
45
+ return function (snapshot) {
46
+ if (hasValue && Object.is(prevSnapshot, snapshot)) {
47
+ return prevSelection;
48
+ }
49
+ var next = selector(snapshot);
50
+ if (hasValue && equals(prevSelection, next)) {
51
+ prevSnapshot = snapshot;
52
+ return prevSelection;
53
+ }
54
+ hasValue = true;
55
+ prevSnapshot = snapshot;
56
+ prevSelection = next;
57
+ return next;
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Subscribe to a slice of an atom, re-rendering only when the selected value
63
+ * changes. The optional `equals` comparator dedupes by content (defaults to
64
+ * `Object.is`), so a parent atom that churns references every update will not
65
+ * re-render the component when the selected slice is content-equal.
66
+ */
67
+ function useSelector(atom, selector, equals) {
68
+ if (equals === void 0) {
69
+ equals = Object.is;
70
+ }
71
+ var select = react.useMemo(function () {
72
+ return createSelectorMemo(selector, equals);
73
+ }, [selector, equals]);
74
+ return react.useSyncExternalStore(function (onStoreChange) {
75
+ var subscribed = false;
76
+ var subscription = atom.subscribe(function () {
77
+ if (subscribed) {
78
+ onStoreChange();
79
+ }
80
+ });
81
+ subscribed = true;
82
+ return function () {
83
+ subscription.unsubscribe();
84
+ };
85
+ }, function () {
86
+ return select(atom.value());
87
+ }, function () {
88
+ return select(atom.value());
89
+ });
46
90
  }
47
91
 
48
92
  exports.useAtom = useAtom;
49
- exports.useSelectAtom = useSelectAtom;
93
+ exports.useSelector = useSelector;
50
94
  exports.useSignal = useSignal;
package/dist/react.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { useAtom } from "./useAtom";
2
2
  export { useSignal } from "./useSignal";
3
- export { useSelectAtom } from "./useSelectAtom";
3
+ export { useSelector } from "./useSelector";
4
4
  //# sourceMappingURL=react.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
package/dist/react.mjs CHANGED
@@ -32,9 +32,46 @@ function useSignal(signal, callback, id) {
32
32
  }, [signal, callback, id]);
33
33
  }
34
34
 
35
- function useSelectAtom(atom, key) {
36
- const selectedAtom = useMemo(() => atom.select(key), [atom, key]);
37
- return useAtom(selectedAtom);
35
+ function createSelectorMemo(selector, equals) {
36
+ let hasValue = false;
37
+ let prevSnapshot;
38
+ let prevSelection;
39
+ return snapshot => {
40
+ if (hasValue && Object.is(prevSnapshot, snapshot)) {
41
+ return prevSelection;
42
+ }
43
+ const next = selector(snapshot);
44
+ if (hasValue && equals(prevSelection, next)) {
45
+ prevSnapshot = snapshot;
46
+ return prevSelection;
47
+ }
48
+ hasValue = true;
49
+ prevSnapshot = snapshot;
50
+ prevSelection = next;
51
+ return next;
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Subscribe to a slice of an atom, re-rendering only when the selected value
57
+ * changes. The optional `equals` comparator dedupes by content (defaults to
58
+ * `Object.is`), so a parent atom that churns references every update will not
59
+ * re-render the component when the selected slice is content-equal.
60
+ */
61
+ function useSelector(atom, selector, equals = Object.is) {
62
+ const select = useMemo(() => createSelectorMemo(selector, equals), [selector, equals]);
63
+ return useSyncExternalStore(onStoreChange => {
64
+ let subscribed = false;
65
+ const subscription = atom.subscribe(() => {
66
+ if (subscribed) {
67
+ onStoreChange();
68
+ }
69
+ });
70
+ subscribed = true;
71
+ return () => {
72
+ subscription.unsubscribe();
73
+ };
74
+ }, () => select(atom.value()), () => select(atom.value()));
38
75
  }
39
76
 
40
- export { useAtom, useSelectAtom, useSignal };
77
+ export { useAtom, useSelector, useSignal };
@@ -0,0 +1,10 @@
1
+ import { Equals, ReadOnlyAtom } from "./Atom";
2
+ export declare function createSelectorMemo<T, R>(selector: (value: T) => R, equals: Equals<R>): (snapshot: T) => R;
3
+ /**
4
+ * Subscribe to a slice of an atom, re-rendering only when the selected value
5
+ * changes. The optional `equals` comparator dedupes by content (defaults to
6
+ * `Object.is`), so a parent atom that churns references every update will not
7
+ * re-render the component when the selected slice is content-equal.
8
+ */
9
+ export declare function useSelector<T, R>(atom: ReadOnlyAtom<T>, selector: (value: T) => R, equals?: Equals<R>): R;
10
+ //# sourceMappingURL=useSelector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSelector.d.ts","sourceRoot":"","sources":["../src/useSelector.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAE9C,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,CAAC,EACrC,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,EACzB,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAChB,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAkBpB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,CAAC,EAC9B,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,EACrB,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,EACzB,MAAM,GAAE,MAAM,CAAC,CAAC,CAAa,GAC5B,CAAC,CAqBH"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chem-rx",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "react state primitives with framework-agnostic atoms",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.mjs",
@@ -36,9 +36,14 @@
36
36
  "@babel/preset-typescript": "^7.22.5",
37
37
  "@rollup/plugin-babel": "^6.0.3",
38
38
  "@rollup/plugin-node-resolve": "^15.1.0",
39
+ "@testing-library/dom": "^10.4.1",
40
+ "@testing-library/react": "^16.3.2",
39
41
  "@types/jest": "^29.5.3",
40
42
  "@types/react": "^18.2.20",
41
43
  "jest": "^29.6.2",
44
+ "jest-environment-jsdom": "^29.7.0",
45
+ "react": "^18.3.1",
46
+ "react-dom": "^18.3.1",
42
47
  "rimraf": "^5.0.1",
43
48
  "rollup": "^3.28.0",
44
49
  "rollup-plugin-size-snapshot": "^0.12.0",
@@ -1,5 +0,0 @@
1
- import { BaseAtom, NullableBaseAtom, ReadOnlyAtom } from "./Atom";
2
- export declare function useSelectAtom<T extends {
3
- [key in K]: V;
4
- }, K extends keyof T, V = T[K]>(atom: NullableBaseAtom<T> | BaseAtom<T> | ReadOnlyAtom<T>, key: K): T[K];
5
- //# sourceMappingURL=useSelectAtom.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useSelectAtom.d.ts","sourceRoot":"","sources":["../src/useSelectAtom.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAGlE,wBAAgB,aAAa,CAC3B,CAAC,SACG;KACG,GAAG,IAAI,CAAC,GAAG,CAAC;CACd,EACL,CAAC,SAAS,MAAM,CAAC,EACjB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EACR,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAMzE"}