@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 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;
@@ -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 };
@@ -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 };
@@ -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
+ }