@sigrea/react 0.1.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 +105 -0
- package/dist/index.cjs +103 -0
- package/dist/index.d.cts +14 -0
- package/dist/index.d.mts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.mjs +97 -0
- package/package.json +72 -0
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# @sigrea/react
|
|
2
|
+
|
|
3
|
+
`@sigrea/react` adapts [@sigrea/core](https://www.npmjs.com/package/@sigrea/core) logic modules and signals so they can participate in React components. It wires scope-aware lifecycles to `useEffect`, keeps signal subscriptions aligned with React rendering, and surfaces ergonomic hooks for both shallow and deep reactivity.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @sigrea/react @sigrea/core react react-dom
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
React 18+ and Node.js 20+ are required. Equivalent npm or yarn commands work the same way.
|
|
12
|
+
|
|
13
|
+
## What This Adapter Provides
|
|
14
|
+
|
|
15
|
+
- **Signal readers** – `useSignal` streams signals and computed values into React components.
|
|
16
|
+
- **Deep signal access** – `useDeepSignal` exposes mutable deep signal objects with automatic teardown.
|
|
17
|
+
- **Derived state** – `useComputed` keeps derived values memoized per component instance.
|
|
18
|
+
- **Logic lifecycles** – `useLogic` mounts `defineLogic` factories and binds `onMount` / `onUnmount` to React’s lifecycle.
|
|
19
|
+
- **Snapshots** – `useSnapshot` provides low-level control when you need direct access to signal handlers.
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### Consume a signal
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { signal } from "@sigrea/core";
|
|
27
|
+
import { useSignal } from "@sigrea/react";
|
|
28
|
+
|
|
29
|
+
const count = signal(0);
|
|
30
|
+
|
|
31
|
+
export function CounterLabel() {
|
|
32
|
+
const value = useSignal(count);
|
|
33
|
+
return <span>{value}</span>;
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Bridge framework-agnostic logic
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { defineLogic, signal } from "@sigrea/core";
|
|
41
|
+
import { useLogic } from "@sigrea/react";
|
|
42
|
+
|
|
43
|
+
const CounterLogic = defineLogic<{ initialCount: number }>()((props) => {
|
|
44
|
+
const count = signal(props.initialCount);
|
|
45
|
+
|
|
46
|
+
const increment = () => {
|
|
47
|
+
count.value += 1;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const reset = () => {
|
|
51
|
+
count.value = props.initialCount;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return { count, increment, reset };
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export function Counter(props: { initialCount: number }) {
|
|
58
|
+
const counter = useLogic(CounterLogic, props);
|
|
59
|
+
const value = useSignal(counter.count);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div>
|
|
63
|
+
<span>{value}</span>
|
|
64
|
+
<button onClick={counter.increment}>Increment</button>
|
|
65
|
+
<button onClick={counter.reset}>Reset</button>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Work with deep signals
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { deepSignal } from "@sigrea/core";
|
|
75
|
+
import { useDeepSignal } from "@sigrea/react";
|
|
76
|
+
|
|
77
|
+
const form = deepSignal({ name: "Sigrea" });
|
|
78
|
+
|
|
79
|
+
export function ProfileForm() {
|
|
80
|
+
const state = useDeepSignal(form);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<label>
|
|
84
|
+
Name
|
|
85
|
+
<input
|
|
86
|
+
value={state.name}
|
|
87
|
+
onChange={(event) => {
|
|
88
|
+
state.name = event.target.value;
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
</label>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Testing
|
|
97
|
+
|
|
98
|
+
- `pnpm install` – install dependencies
|
|
99
|
+
- `pnpm test` – run the Vitest suite
|
|
100
|
+
- `pnpm build` – emit distributable artifacts
|
|
101
|
+
- `pnpm dev` – launch the playground counter demo
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT — see `LICENSE`.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const react = require('react');
|
|
4
|
+
const core = require('@sigrea/core');
|
|
5
|
+
|
|
6
|
+
const scheduleMicrotask = (callback) => {
|
|
7
|
+
if (typeof globalThis.queueMicrotask === "function") {
|
|
8
|
+
globalThis.queueMicrotask(callback);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
Promise.resolve().then(callback);
|
|
12
|
+
};
|
|
13
|
+
function useLogic(logic, ...args) {
|
|
14
|
+
const props = args.length === 0 ? void 0 : args[0];
|
|
15
|
+
const stateRef = react.useRef(void 0);
|
|
16
|
+
const currentState = stateRef.current;
|
|
17
|
+
const shouldRemount = currentState === void 0 || currentState.logic !== logic || !Object.is(currentState.props, props);
|
|
18
|
+
if (shouldRemount) {
|
|
19
|
+
if (currentState !== void 0) {
|
|
20
|
+
core.cleanupLogic(currentState.instance);
|
|
21
|
+
stateRef.current = void 0;
|
|
22
|
+
}
|
|
23
|
+
const logicArgs = props === void 0 ? [] : [props];
|
|
24
|
+
stateRef.current = {
|
|
25
|
+
instance: core.mountLogic(logic, ...logicArgs),
|
|
26
|
+
logic,
|
|
27
|
+
props,
|
|
28
|
+
cleanupScheduled: false,
|
|
29
|
+
cleanupToken: 0
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const state = stateRef.current;
|
|
33
|
+
if (state === void 0) {
|
|
34
|
+
throw new Error("useLogic failed to mount the requested logic instance.");
|
|
35
|
+
}
|
|
36
|
+
const instance = state.instance;
|
|
37
|
+
react.useEffect(() => {
|
|
38
|
+
const state2 = stateRef.current;
|
|
39
|
+
if (state2 === void 0 || state2.instance !== instance) {
|
|
40
|
+
return () => {
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
state2.cleanupScheduled = false;
|
|
44
|
+
return () => {
|
|
45
|
+
const latest = stateRef.current;
|
|
46
|
+
if (latest === void 0 || latest.instance !== instance) {
|
|
47
|
+
core.cleanupLogic(instance);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
latest.cleanupScheduled = true;
|
|
51
|
+
const token = latest.cleanupToken + 1;
|
|
52
|
+
latest.cleanupToken = token;
|
|
53
|
+
scheduleMicrotask(() => {
|
|
54
|
+
const updated = stateRef.current;
|
|
55
|
+
if (updated === void 0 || updated.instance !== instance || !updated.cleanupScheduled || updated.cleanupToken !== token) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
updated.cleanupScheduled = false;
|
|
59
|
+
stateRef.current = void 0;
|
|
60
|
+
core.cleanupLogic(instance);
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
}, [instance]);
|
|
64
|
+
return instance;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function useSnapshot(handler) {
|
|
68
|
+
const subscribe = react.useCallback(
|
|
69
|
+
(onStoreChange) => handler.subscribe(onStoreChange),
|
|
70
|
+
[handler]
|
|
71
|
+
);
|
|
72
|
+
const getSnapshot = react.useCallback(() => handler.getSnapshot(), [handler]);
|
|
73
|
+
const snapshot = react.useSyncExternalStore(
|
|
74
|
+
subscribe,
|
|
75
|
+
getSnapshot,
|
|
76
|
+
getSnapshot
|
|
77
|
+
);
|
|
78
|
+
return snapshot.value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function useSignal(source) {
|
|
82
|
+
const handler = react.useMemo(
|
|
83
|
+
() => core.createSignalHandler(source),
|
|
84
|
+
[source]
|
|
85
|
+
);
|
|
86
|
+
return useSnapshot(handler);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function useComputed(source) {
|
|
90
|
+
const handler = react.useMemo(() => core.createComputedHandler(source), [source]);
|
|
91
|
+
return useSnapshot(handler);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function useDeepSignal(source) {
|
|
95
|
+
const handler = react.useMemo(() => core.createDeepSignalHandler(source), [source]);
|
|
96
|
+
return useSnapshot(handler);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
exports.useComputed = useComputed;
|
|
100
|
+
exports.useDeepSignal = useDeepSignal;
|
|
101
|
+
exports.useLogic = useLogic;
|
|
102
|
+
exports.useSignal = useSignal;
|
|
103
|
+
exports.useSnapshot = useSnapshot;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { LogicFunction, LogicArgs, LogicInstance, Signal, ReadonlySignal, Computed, DeepSignal, SnapshotHandler } from '@sigrea/core';
|
|
2
|
+
|
|
3
|
+
declare function useLogic<TReturn extends object, TProps = void>(logic: LogicFunction<TReturn, TProps>, ...args: LogicArgs<TProps>): LogicInstance<TReturn>;
|
|
4
|
+
|
|
5
|
+
type ReadableSignal<T> = Signal<T> | ReadonlySignal<T>;
|
|
6
|
+
declare function useSignal<T>(source: ReadableSignal<T>): T;
|
|
7
|
+
|
|
8
|
+
declare function useComputed<T>(source: Computed<T>): T;
|
|
9
|
+
|
|
10
|
+
declare function useDeepSignal<T extends object>(source: DeepSignal<T>): T;
|
|
11
|
+
|
|
12
|
+
declare function useSnapshot<T>(handler: SnapshotHandler<T>): T;
|
|
13
|
+
|
|
14
|
+
export { useComputed, useDeepSignal, useLogic, useSignal, useSnapshot };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { LogicFunction, LogicArgs, LogicInstance, Signal, ReadonlySignal, Computed, DeepSignal, SnapshotHandler } from '@sigrea/core';
|
|
2
|
+
|
|
3
|
+
declare function useLogic<TReturn extends object, TProps = void>(logic: LogicFunction<TReturn, TProps>, ...args: LogicArgs<TProps>): LogicInstance<TReturn>;
|
|
4
|
+
|
|
5
|
+
type ReadableSignal<T> = Signal<T> | ReadonlySignal<T>;
|
|
6
|
+
declare function useSignal<T>(source: ReadableSignal<T>): T;
|
|
7
|
+
|
|
8
|
+
declare function useComputed<T>(source: Computed<T>): T;
|
|
9
|
+
|
|
10
|
+
declare function useDeepSignal<T extends object>(source: DeepSignal<T>): T;
|
|
11
|
+
|
|
12
|
+
declare function useSnapshot<T>(handler: SnapshotHandler<T>): T;
|
|
13
|
+
|
|
14
|
+
export { useComputed, useDeepSignal, useLogic, useSignal, useSnapshot };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { LogicFunction, LogicArgs, LogicInstance, Signal, ReadonlySignal, Computed, DeepSignal, SnapshotHandler } from '@sigrea/core';
|
|
2
|
+
|
|
3
|
+
declare function useLogic<TReturn extends object, TProps = void>(logic: LogicFunction<TReturn, TProps>, ...args: LogicArgs<TProps>): LogicInstance<TReturn>;
|
|
4
|
+
|
|
5
|
+
type ReadableSignal<T> = Signal<T> | ReadonlySignal<T>;
|
|
6
|
+
declare function useSignal<T>(source: ReadableSignal<T>): T;
|
|
7
|
+
|
|
8
|
+
declare function useComputed<T>(source: Computed<T>): T;
|
|
9
|
+
|
|
10
|
+
declare function useDeepSignal<T extends object>(source: DeepSignal<T>): T;
|
|
11
|
+
|
|
12
|
+
declare function useSnapshot<T>(handler: SnapshotHandler<T>): T;
|
|
13
|
+
|
|
14
|
+
export { useComputed, useDeepSignal, useLogic, useSignal, useSnapshot };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useRef, useEffect, useCallback, useSyncExternalStore, useMemo } from 'react';
|
|
2
|
+
import { cleanupLogic, mountLogic, createSignalHandler, createComputedHandler, createDeepSignalHandler } from '@sigrea/core';
|
|
3
|
+
|
|
4
|
+
const scheduleMicrotask = (callback) => {
|
|
5
|
+
if (typeof globalThis.queueMicrotask === "function") {
|
|
6
|
+
globalThis.queueMicrotask(callback);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
Promise.resolve().then(callback);
|
|
10
|
+
};
|
|
11
|
+
function useLogic(logic, ...args) {
|
|
12
|
+
const props = args.length === 0 ? void 0 : args[0];
|
|
13
|
+
const stateRef = useRef(void 0);
|
|
14
|
+
const currentState = stateRef.current;
|
|
15
|
+
const shouldRemount = currentState === void 0 || currentState.logic !== logic || !Object.is(currentState.props, props);
|
|
16
|
+
if (shouldRemount) {
|
|
17
|
+
if (currentState !== void 0) {
|
|
18
|
+
cleanupLogic(currentState.instance);
|
|
19
|
+
stateRef.current = void 0;
|
|
20
|
+
}
|
|
21
|
+
const logicArgs = props === void 0 ? [] : [props];
|
|
22
|
+
stateRef.current = {
|
|
23
|
+
instance: mountLogic(logic, ...logicArgs),
|
|
24
|
+
logic,
|
|
25
|
+
props,
|
|
26
|
+
cleanupScheduled: false,
|
|
27
|
+
cleanupToken: 0
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const state = stateRef.current;
|
|
31
|
+
if (state === void 0) {
|
|
32
|
+
throw new Error("useLogic failed to mount the requested logic instance.");
|
|
33
|
+
}
|
|
34
|
+
const instance = state.instance;
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const state2 = stateRef.current;
|
|
37
|
+
if (state2 === void 0 || state2.instance !== instance) {
|
|
38
|
+
return () => {
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
state2.cleanupScheduled = false;
|
|
42
|
+
return () => {
|
|
43
|
+
const latest = stateRef.current;
|
|
44
|
+
if (latest === void 0 || latest.instance !== instance) {
|
|
45
|
+
cleanupLogic(instance);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
latest.cleanupScheduled = true;
|
|
49
|
+
const token = latest.cleanupToken + 1;
|
|
50
|
+
latest.cleanupToken = token;
|
|
51
|
+
scheduleMicrotask(() => {
|
|
52
|
+
const updated = stateRef.current;
|
|
53
|
+
if (updated === void 0 || updated.instance !== instance || !updated.cleanupScheduled || updated.cleanupToken !== token) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
updated.cleanupScheduled = false;
|
|
57
|
+
stateRef.current = void 0;
|
|
58
|
+
cleanupLogic(instance);
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
}, [instance]);
|
|
62
|
+
return instance;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function useSnapshot(handler) {
|
|
66
|
+
const subscribe = useCallback(
|
|
67
|
+
(onStoreChange) => handler.subscribe(onStoreChange),
|
|
68
|
+
[handler]
|
|
69
|
+
);
|
|
70
|
+
const getSnapshot = useCallback(() => handler.getSnapshot(), [handler]);
|
|
71
|
+
const snapshot = useSyncExternalStore(
|
|
72
|
+
subscribe,
|
|
73
|
+
getSnapshot,
|
|
74
|
+
getSnapshot
|
|
75
|
+
);
|
|
76
|
+
return snapshot.value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function useSignal(source) {
|
|
80
|
+
const handler = useMemo(
|
|
81
|
+
() => createSignalHandler(source),
|
|
82
|
+
[source]
|
|
83
|
+
);
|
|
84
|
+
return useSnapshot(handler);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function useComputed(source) {
|
|
88
|
+
const handler = useMemo(() => createComputedHandler(source), [source]);
|
|
89
|
+
return useSnapshot(handler);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function useDeepSignal(source) {
|
|
93
|
+
const handler = useMemo(() => createDeepSignalHandler(source), [source]);
|
|
94
|
+
return useSnapshot(handler);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export { useComputed, useDeepSignal, useLogic, useSignal, useSnapshot };
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sigrea/react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React adapter bindings for Sigrea logic modules.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/sigrea/react.git"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/sigrea/react#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/sigrea/react/issues"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.mjs",
|
|
26
|
+
"require": "./dist/index.cjs"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"main": "./dist/index.cjs",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"keywords": [
|
|
35
|
+
"signals",
|
|
36
|
+
"reactivity",
|
|
37
|
+
"react",
|
|
38
|
+
"logic",
|
|
39
|
+
"typescript"
|
|
40
|
+
],
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@sigrea/core": "^0.1.0",
|
|
43
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
44
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@biomejs/biome": "1.9.4",
|
|
48
|
+
"@vitejs/plugin-react": "^4.3.3",
|
|
49
|
+
"@changesets/cli": "^2.29.6",
|
|
50
|
+
"@types/react": "^19.0.0",
|
|
51
|
+
"@types/react-dom": "^19.0.0",
|
|
52
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
53
|
+
"lefthook": "1.13.6",
|
|
54
|
+
"react": "^19.0.0",
|
|
55
|
+
"react-dom": "^19.0.0",
|
|
56
|
+
"tsx": "^4.20.5",
|
|
57
|
+
"typescript": "5.9.3",
|
|
58
|
+
"unbuild": "3.6.1",
|
|
59
|
+
"vite": "^5.4.6",
|
|
60
|
+
"vitest": "^3.2.4",
|
|
61
|
+
"jsdom": "^24.1.3"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"dev": "vite --config playground/vite.config.ts",
|
|
65
|
+
"build": "unbuild",
|
|
66
|
+
"test": "vitest run",
|
|
67
|
+
"test:coverage": "vitest --coverage",
|
|
68
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
69
|
+
"format": "biome check .",
|
|
70
|
+
"format:fix": "biome check --write ."
|
|
71
|
+
}
|
|
72
|
+
}
|