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 +16 -0
- package/README.md +24 -5
- package/dist/react.cjs.js +50 -6
- package/dist/react.d.ts +1 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.mjs +41 -4
- package/dist/useSelector.d.ts +10 -0
- package/dist/useSelector.d.ts.map +1 -0
- package/package.json +6 -1
- package/dist/useSelectAtom.d.ts +0 -5
- package/dist/useSelectAtom.d.ts.map +0 -1
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
|
-
###
|
|
392
|
+
### useSelector
|
|
393
393
|
|
|
394
|
-
With `
|
|
395
|
-
update
|
|
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 {
|
|
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 =
|
|
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
|
|
42
|
-
var
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return
|
|
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.
|
|
93
|
+
exports.useSelector = useSelector;
|
|
50
94
|
exports.useSignal = useSignal;
|
package/dist/react.d.ts
CHANGED
package/dist/react.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
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,
|
|
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.
|
|
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",
|
package/dist/useSelectAtom.d.ts
DELETED
|
@@ -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"}
|