@sigrea/react 0.5.0 → 0.6.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/LICENSE +21 -0
- package/README.md +13 -9
- package/dist/index.cjs +21 -11
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +21 -11
- package/package.json +7 -18
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sigrea
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @sigrea/react
|
|
2
2
|
|
|
3
|
-
`@sigrea/react` adapts [@sigrea/core](https://www.npmjs.com/package/@sigrea/core) molecule modules and signals for use in React components. It binds scope-aware lifecycles to
|
|
3
|
+
`@sigrea/react` adapts [@sigrea/core](https://www.npmjs.com/package/@sigrea/core) molecule modules and signals for use in React components. It binds scope-aware lifecycles to React commits, synchronizes signal subscriptions with React rendering, and provides hooks for both shallow and deep reactivity.
|
|
4
4
|
|
|
5
5
|
- **Signal subscriptions.** `useSignal` subscribes to signals and computed values, triggering re-renders when they change.
|
|
6
6
|
- **Computed subscriptions.** `useComputed` subscribes to computed values and memoizes them per component instance.
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
npm install @sigrea/react @sigrea/core react react-dom
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
Requires React 18+ and Node.js
|
|
33
|
+
Requires React 18+ and Node.js 24 or later.
|
|
34
34
|
|
|
35
35
|
## Quick Start
|
|
36
36
|
|
|
@@ -130,10 +130,12 @@ export function ProfileForm() {
|
|
|
130
130
|
### useSignal
|
|
131
131
|
|
|
132
132
|
```tsx
|
|
133
|
-
function useSignal<T>(
|
|
133
|
+
function useSignal<T>(
|
|
134
|
+
signal: Signal<T> | ReadonlySignal<T> | Computed<T>
|
|
135
|
+
): T
|
|
134
136
|
```
|
|
135
137
|
|
|
136
|
-
Subscribes to a signal or computed value and returns its current value. The component re-renders when the
|
|
138
|
+
Subscribes to a signal or computed value and returns its current value. The component re-renders when the source changes.
|
|
137
139
|
|
|
138
140
|
### useComputed
|
|
139
141
|
|
|
@@ -141,7 +143,7 @@ Subscribes to a signal or computed value and returns its current value. The comp
|
|
|
141
143
|
function useComputed<T>(source: Computed<T>): T
|
|
142
144
|
```
|
|
143
145
|
|
|
144
|
-
Subscribes to a computed value and returns its current value.
|
|
146
|
+
Subscribes to a computed value and returns its current value. This behaves like `useSignal(source)` for computed sources, but keeps the call site explicit when the source is known to be computed.
|
|
145
147
|
|
|
146
148
|
### useDeepSignal
|
|
147
149
|
|
|
@@ -164,10 +166,12 @@ Mounts a molecule factory and returns its MoleculeInstance. The molecule's scope
|
|
|
164
166
|
|
|
165
167
|
**Lifecycle Timing**
|
|
166
168
|
|
|
167
|
-
Molecule lifecycles are bound to React
|
|
169
|
+
Molecule lifecycles are bound to React commits for precise timing control:
|
|
168
170
|
|
|
169
|
-
- In **browser environments**, molecule mounting happens synchronously after
|
|
170
|
-
- In **SSR environments**,
|
|
171
|
+
- In **browser environments**, molecule mounting happens synchronously after the component commits but before paint (via `useLayoutEffect`). This matches Vue 3's `onMounted` timing, ensuring consistent behavior across frameworks.
|
|
172
|
+
- In **SSR environments**, `useMolecule` supports both `renderToString` and streaming server rendering (`renderToPipeableStream`, `renderToReadableStream`). The molecule instance is created during render so components can read its state, but it is never mounted on the server.
|
|
173
|
+
- `onMount`, `watch`, and `watchEffect` registered during setup do not run during server rendering.
|
|
174
|
+
- After a **server render** finishes, the unmounted molecule instance is disposed automatically in a microtask so setup-scope `onDispose` cleanups do not leak across requests.
|
|
171
175
|
|
|
172
176
|
This design ensures that `onMount` callbacks and `watch` effects activate at the right moment—early enough to set up subscriptions before the first paint, yet safely after the component has committed to the DOM.
|
|
173
177
|
|
|
@@ -220,7 +224,7 @@ createRoot(document.getElementById("root")!).render(<App />);
|
|
|
220
224
|
|
|
221
225
|
## Development
|
|
222
226
|
|
|
223
|
-
This repo targets Node.js
|
|
227
|
+
This repo targets Node.js 24 or later.
|
|
224
228
|
|
|
225
229
|
If you use mise:
|
|
226
230
|
|
package/dist/index.cjs
CHANGED
|
@@ -4,6 +4,19 @@ const react = require('react');
|
|
|
4
4
|
const core = require('@sigrea/core');
|
|
5
5
|
|
|
6
6
|
const useIsomorphicLayoutEffect = typeof window !== "undefined" ? react.useLayoutEffect : react.useEffect;
|
|
7
|
+
const isServerEnvironment = typeof window === "undefined";
|
|
8
|
+
function schedulePendingDispose(stateRef, instance, token) {
|
|
9
|
+
queueMicrotask(() => {
|
|
10
|
+
const current = stateRef.current;
|
|
11
|
+
if (current === void 0 || current.instance !== instance || current.subscribers > 0 || current.disposed || current.pendingDisposeToken !== token) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
current.disposed = true;
|
|
15
|
+
current.pendingDisposeToken = null;
|
|
16
|
+
stateRef.current = void 0;
|
|
17
|
+
core.disposeMolecule(instance);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
7
20
|
function useMolecule(molecule, ...args) {
|
|
8
21
|
const props = args.length === 0 ? void 0 : args[0];
|
|
9
22
|
if (props !== void 0 && (typeof props !== "object" || props === null)) {
|
|
@@ -22,13 +35,19 @@ function useMolecule(molecule, ...args) {
|
|
|
22
35
|
}
|
|
23
36
|
const snapshot = props === void 0 ? void 0 : { ...props };
|
|
24
37
|
const moleculeArgs = snapshot === void 0 ? [] : [snapshot];
|
|
25
|
-
|
|
38
|
+
const nextState = {
|
|
26
39
|
instance: molecule(...moleculeArgs),
|
|
27
40
|
molecule,
|
|
28
41
|
subscribers: 0,
|
|
29
42
|
disposed: false,
|
|
30
43
|
pendingDisposeToken: null
|
|
31
44
|
};
|
|
45
|
+
stateRef.current = nextState;
|
|
46
|
+
if (isServerEnvironment) {
|
|
47
|
+
const token = Symbol("pending-server-dispose");
|
|
48
|
+
nextState.pendingDisposeToken = token;
|
|
49
|
+
schedulePendingDispose(stateRef, nextState.instance, token);
|
|
50
|
+
}
|
|
32
51
|
}
|
|
33
52
|
const state = stateRef.current;
|
|
34
53
|
if (state === void 0) {
|
|
@@ -64,16 +83,7 @@ function useMolecule(molecule, ...args) {
|
|
|
64
83
|
core.unmountMolecule(instance);
|
|
65
84
|
const token = Symbol("pending-dispose");
|
|
66
85
|
latest.pendingDisposeToken = token;
|
|
67
|
-
|
|
68
|
-
const current = stateRef.current;
|
|
69
|
-
if (current === void 0 || current.instance !== instance || current.subscribers > 0 || current.disposed || current.pendingDisposeToken !== token) {
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
current.disposed = true;
|
|
73
|
-
current.pendingDisposeToken = null;
|
|
74
|
-
stateRef.current = void 0;
|
|
75
|
-
core.disposeMolecule(instance);
|
|
76
|
-
});
|
|
86
|
+
schedulePendingDispose(stateRef, instance, token);
|
|
77
87
|
}
|
|
78
88
|
};
|
|
79
89
|
}, [instance]);
|
package/dist/index.d.cts
CHANGED
|
@@ -2,7 +2,7 @@ import { MoleculeFactory, MoleculeArgs, MoleculeInstance, Signal, ReadonlySignal
|
|
|
2
2
|
|
|
3
3
|
declare function useMolecule<TReturn extends object, TProps extends object | void = void>(molecule: MoleculeFactory<TReturn, TProps>, ...args: MoleculeArgs<TProps>): MoleculeInstance<TReturn>;
|
|
4
4
|
|
|
5
|
-
type ReadableSignal<T> = Signal<T> | ReadonlySignal<T>;
|
|
5
|
+
type ReadableSignal<T> = Signal<T> | ReadonlySignal<T> | Computed<T>;
|
|
6
6
|
declare function useSignal<T>(source: ReadableSignal<T>): T;
|
|
7
7
|
|
|
8
8
|
declare function useComputed<T>(source: Computed<T>): T;
|
package/dist/index.d.mts
CHANGED
|
@@ -2,7 +2,7 @@ import { MoleculeFactory, MoleculeArgs, MoleculeInstance, Signal, ReadonlySignal
|
|
|
2
2
|
|
|
3
3
|
declare function useMolecule<TReturn extends object, TProps extends object | void = void>(molecule: MoleculeFactory<TReturn, TProps>, ...args: MoleculeArgs<TProps>): MoleculeInstance<TReturn>;
|
|
4
4
|
|
|
5
|
-
type ReadableSignal<T> = Signal<T> | ReadonlySignal<T>;
|
|
5
|
+
type ReadableSignal<T> = Signal<T> | ReadonlySignal<T> | Computed<T>;
|
|
6
6
|
declare function useSignal<T>(source: ReadableSignal<T>): T;
|
|
7
7
|
|
|
8
8
|
declare function useComputed<T>(source: Computed<T>): T;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { MoleculeFactory, MoleculeArgs, MoleculeInstance, Signal, ReadonlySignal
|
|
|
2
2
|
|
|
3
3
|
declare function useMolecule<TReturn extends object, TProps extends object | void = void>(molecule: MoleculeFactory<TReturn, TProps>, ...args: MoleculeArgs<TProps>): MoleculeInstance<TReturn>;
|
|
4
4
|
|
|
5
|
-
type ReadableSignal<T> = Signal<T> | ReadonlySignal<T>;
|
|
5
|
+
type ReadableSignal<T> = Signal<T> | ReadonlySignal<T> | Computed<T>;
|
|
6
6
|
declare function useSignal<T>(source: ReadableSignal<T>): T;
|
|
7
7
|
|
|
8
8
|
declare function useComputed<T>(source: Computed<T>): T;
|
package/dist/index.mjs
CHANGED
|
@@ -2,6 +2,19 @@ import { useRef, useLayoutEffect, useEffect, useCallback, useSyncExternalStore,
|
|
|
2
2
|
import { disposeMolecule, mountMolecule, unmountMolecule, createSignalHandler, createComputedHandler, createDeepSignalHandler } from '@sigrea/core';
|
|
3
3
|
|
|
4
4
|
const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
5
|
+
const isServerEnvironment = typeof window === "undefined";
|
|
6
|
+
function schedulePendingDispose(stateRef, instance, token) {
|
|
7
|
+
queueMicrotask(() => {
|
|
8
|
+
const current = stateRef.current;
|
|
9
|
+
if (current === void 0 || current.instance !== instance || current.subscribers > 0 || current.disposed || current.pendingDisposeToken !== token) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
current.disposed = true;
|
|
13
|
+
current.pendingDisposeToken = null;
|
|
14
|
+
stateRef.current = void 0;
|
|
15
|
+
disposeMolecule(instance);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
5
18
|
function useMolecule(molecule, ...args) {
|
|
6
19
|
const props = args.length === 0 ? void 0 : args[0];
|
|
7
20
|
if (props !== void 0 && (typeof props !== "object" || props === null)) {
|
|
@@ -20,13 +33,19 @@ function useMolecule(molecule, ...args) {
|
|
|
20
33
|
}
|
|
21
34
|
const snapshot = props === void 0 ? void 0 : { ...props };
|
|
22
35
|
const moleculeArgs = snapshot === void 0 ? [] : [snapshot];
|
|
23
|
-
|
|
36
|
+
const nextState = {
|
|
24
37
|
instance: molecule(...moleculeArgs),
|
|
25
38
|
molecule,
|
|
26
39
|
subscribers: 0,
|
|
27
40
|
disposed: false,
|
|
28
41
|
pendingDisposeToken: null
|
|
29
42
|
};
|
|
43
|
+
stateRef.current = nextState;
|
|
44
|
+
if (isServerEnvironment) {
|
|
45
|
+
const token = Symbol("pending-server-dispose");
|
|
46
|
+
nextState.pendingDisposeToken = token;
|
|
47
|
+
schedulePendingDispose(stateRef, nextState.instance, token);
|
|
48
|
+
}
|
|
30
49
|
}
|
|
31
50
|
const state = stateRef.current;
|
|
32
51
|
if (state === void 0) {
|
|
@@ -62,16 +81,7 @@ function useMolecule(molecule, ...args) {
|
|
|
62
81
|
unmountMolecule(instance);
|
|
63
82
|
const token = Symbol("pending-dispose");
|
|
64
83
|
latest.pendingDisposeToken = token;
|
|
65
|
-
|
|
66
|
-
const current = stateRef.current;
|
|
67
|
-
if (current === void 0 || current.instance !== instance || current.subscribers > 0 || current.disposed || current.pendingDisposeToken !== token) {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
current.disposed = true;
|
|
71
|
-
current.pendingDisposeToken = null;
|
|
72
|
-
stateRef.current = void 0;
|
|
73
|
-
disposeMolecule(instance);
|
|
74
|
-
});
|
|
84
|
+
schedulePendingDispose(stateRef, instance, token);
|
|
75
85
|
}
|
|
76
86
|
};
|
|
77
87
|
}, [instance]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sigrea/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "React adapter bindings for Sigrea molecule modules.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"url": "https://github.com/sigrea/react/issues"
|
|
18
18
|
},
|
|
19
19
|
"engines": {
|
|
20
|
-
"node": ">=
|
|
20
|
+
"node": ">=24"
|
|
21
21
|
},
|
|
22
22
|
"sideEffects": false,
|
|
23
23
|
"exports": {
|
|
@@ -29,16 +29,8 @@
|
|
|
29
29
|
},
|
|
30
30
|
"main": "./dist/index.cjs",
|
|
31
31
|
"types": "./dist/index.d.ts",
|
|
32
|
-
"files": [
|
|
33
|
-
|
|
34
|
-
],
|
|
35
|
-
"keywords": [
|
|
36
|
-
"signals",
|
|
37
|
-
"reactivity",
|
|
38
|
-
"react",
|
|
39
|
-
"molecule",
|
|
40
|
-
"typescript"
|
|
41
|
-
],
|
|
32
|
+
"files": ["dist"],
|
|
33
|
+
"keywords": ["signals", "reactivity", "react", "molecule", "typescript"],
|
|
42
34
|
"scripts": {
|
|
43
35
|
"dev": "vite --config playground/vite.config.ts",
|
|
44
36
|
"build": "unbuild",
|
|
@@ -53,13 +45,13 @@
|
|
|
53
45
|
"cicheck": "pnpm test && pnpm typecheck && pnpm format:fix"
|
|
54
46
|
},
|
|
55
47
|
"peerDependencies": {
|
|
56
|
-
"@sigrea/core": "^0.
|
|
48
|
+
"@sigrea/core": "^0.6.0",
|
|
57
49
|
"react": "^18.0.0 || ^19.0.0",
|
|
58
50
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
59
51
|
},
|
|
60
52
|
"devDependencies": {
|
|
61
53
|
"@biomejs/biome": "1.9.4",
|
|
62
|
-
"@sigrea/core": "^0.
|
|
54
|
+
"@sigrea/core": "^0.6.0",
|
|
63
55
|
"@types/react": "^19.0.0",
|
|
64
56
|
"@types/react-dom": "^19.0.0",
|
|
65
57
|
"@vitejs/plugin-react": "^4.3.3",
|
|
@@ -77,9 +69,6 @@
|
|
|
77
69
|
"vitest": "^3.2.4"
|
|
78
70
|
},
|
|
79
71
|
"pnpm": {
|
|
80
|
-
"onlyBuiltDependencies": [
|
|
81
|
-
"lefthook",
|
|
82
|
-
"@biomejs/biome"
|
|
83
|
-
]
|
|
72
|
+
"onlyBuiltDependencies": ["lefthook", "@biomejs/biome"]
|
|
84
73
|
}
|
|
85
74
|
}
|