@sigrea/react 0.2.1 → 0.3.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/README.md CHANGED
@@ -1,25 +1,26 @@
1
1
  # @sigrea/react
2
2
 
3
- `@sigrea/react` adapts [@sigrea/core](https://www.npmjs.com/package/@sigrea/core) logic modules and signals for use in React components. It binds scope-aware lifecycles to `useEffect`, synchronizes signal subscriptions with React rendering, and provides hooks for both shallow and deep reactivity.
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 `useEffect`, 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.
7
7
  - **Deep signal subscriptions.** `useDeepSignal` subscribes to deep signal objects and exposes them for direct mutation.
8
- - **Logic lifecycles.** `useLogic` mounts logic factories and binds their lifecycles to React components.
8
+ - **Molecule lifecycles.** `useMolcule` mounts molecule factories and binds their lifecycles to React components.
9
9
 
10
10
  ## Table of Contents
11
11
 
12
12
  - [Install](#install)
13
13
  - [Quick Start](#quick-start)
14
14
  - [Consume a Signal](#consume-a-signal)
15
- - [Bridge Framework-Agnostic Logic](#bridge-framework-agnostic-logic)
15
+ - [Bridge Framework-Agnostic Molecules](#bridge-framework-agnostic-molecules)
16
16
  - [Work with Deep Signals](#work-with-deep-signals)
17
17
  - [API Reference](#api-reference)
18
18
  - [useSignal](#usesignal)
19
19
  - [useComputed](#usecomputed)
20
20
  - [useDeepSignal](#usedeepsignal)
21
- - [useLogic](#uselogic)
21
+ - [useMolcule](#usemolcule)
22
22
  - [Testing](#testing)
23
+ - [Handling Scope Cleanup Errors](#handling-scope-cleanup-errors)
23
24
  - [Development](#development)
24
25
  - [License](#license)
25
26
 
@@ -47,13 +48,13 @@ export function CounterLabel() {
47
48
  }
48
49
  ```
49
50
 
50
- ### Bridge Framework-Agnostic Logic
51
+ ### Bridge Framework-Agnostic Molecules
51
52
 
52
53
  ```tsx
53
- import { defineLogic, signal } from "@sigrea/core";
54
- import { useLogic, useSignal } from "@sigrea/react";
54
+ import { molecule, signal } from "@sigrea/core";
55
+ import { useMolcule, useSignal } from "@sigrea/react";
55
56
 
56
- const CounterLogic = defineLogic<{ initialCount: number }>()((props) => {
57
+ const CounterMolecule = molecule((props: { initialCount: number }) => {
57
58
  const count = signal(props.initialCount);
58
59
 
59
60
  const increment = () => {
@@ -68,7 +69,7 @@ const CounterLogic = defineLogic<{ initialCount: number }>()((props) => {
68
69
  });
69
70
 
70
71
  export function Counter(props: { initialCount: number }) {
71
- const counter = useLogic(CounterLogic, props);
72
+ const counter = useMolcule(CounterMolecule, props);
72
73
  const value = useSignal(counter.count);
73
74
 
74
75
  return (
@@ -132,27 +133,24 @@ function useDeepSignal<T extends object>(signal: DeepSignal<T>): T
132
133
 
133
134
  Exposes a deep signal object for direct mutation within the component. Updates to nested properties trigger re-renders, and the subscription is cleaned up when the component unmounts.
134
135
 
135
- ### useLogic
136
+ ### useMolcule
136
137
 
137
138
  ```tsx
138
- function useLogic<TProps, TReturn>(
139
- logic: LogicFunction<TProps, TReturn>,
139
+ function useMolcule<TProps, TReturn>(
140
+ molecule: MoleculeFactory<TProps, TReturn>,
140
141
  props?: TProps
141
142
  ): TReturn
142
143
  ```
143
144
 
144
- Mounts a logic factory and returns its public API. The logic's scope is bound to the component lifecycle: `onMount` callbacks run after the component mounts, and `onUnmount` callbacks run before it unmounts.
145
+ Mounts a molecule factory and returns its public API. The molecule's scope is bound to the component lifecycle: `onMount` callbacks run after the component mounts, and `onUnmount` callbacks run before it unmounts.
145
146
 
146
147
  ## Testing
147
148
 
148
149
  ```tsx
149
150
  // tests/Counter.test.tsx
150
151
  import { render, screen, fireEvent } from "@testing-library/react";
151
- import { cleanupLogics } from "@sigrea/core";
152
152
  import { Counter } from "../components/Counter";
153
153
 
154
- afterEach(() => cleanupLogics());
155
-
156
154
  it("increments and displays the updated count", () => {
157
155
  render(<Counter initialCount={10} />);
158
156
 
@@ -163,15 +161,54 @@ it("increments and displays the updated count", () => {
163
161
  });
164
162
  ```
165
163
 
164
+ ## Handling Scope Cleanup Errors
165
+
166
+ For global error handling configuration, see [@sigrea/core - Handling Scope Cleanup Errors](https://github.com/sigrea/core#handling-scope-cleanup-errors).
167
+
168
+ In React apps, configure the handler in your application entry point before rendering:
169
+
170
+ ```tsx
171
+ // index.tsx or main.tsx
172
+ import { setScopeCleanupErrorHandler } from "@sigrea/core";
173
+ import { createRoot } from "react-dom/client";
174
+ import { App } from "./App";
175
+
176
+ setScopeCleanupErrorHandler((error, context) => {
177
+ console.error(`Cleanup failed:`, error);
178
+
179
+ // Forward to monitoring service
180
+ if (typeof Sentry !== "undefined") {
181
+ Sentry.captureException(error, {
182
+ tags: { scopeId: context.scopeId, phase: context.phase },
183
+ });
184
+ }
185
+ });
186
+
187
+ createRoot(document.getElementById("root")!).render(<App />);
188
+ ```
189
+
166
190
  ## Development
167
191
 
168
- Development scripts prefer pnpm. npm or yarn work too, but pnpm keeps dependency resolution identical to CI.
192
+ This repo targets Node.js 20 or later.
193
+
194
+ If you use mise:
195
+
196
+ - `mise trust -y` — trust `mise.toml` (first run only).
197
+ - `mise run ci` — run CI-equivalent checks locally.
198
+ - `mise run notes` — preview release notes (optional).
199
+
200
+ You can also run pnpm scripts directly:
169
201
 
170
202
  - `pnpm install` — install dependencies.
171
203
  - `pnpm test` — run the Vitest suite once (no watch).
204
+ - `pnpm typecheck` — run TypeScript type checking.
205
+ - `pnpm test:coverage` — collect coverage.
172
206
  - `pnpm build` — compile via unbuild to produce dual CJS/ESM bundles.
207
+ - `pnpm cicheck` — run CI checks locally.
173
208
  - `pnpm dev` — launch the playground counter demo.
174
209
 
210
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for workflow details.
211
+
175
212
  ## License
176
213
 
177
214
  MIT — see [LICENSE](./LICENSE).
package/dist/index.cjs CHANGED
@@ -3,21 +3,23 @@
3
3
  const react = require('react');
4
4
  const core = require('@sigrea/core');
5
5
 
6
- function useLogic(logic, ...args) {
6
+ function useMolcule(molecule, ...args) {
7
7
  const props = args.length === 0 ? void 0 : args[0];
8
- const stateRef = react.useRef(void 0);
8
+ const stateRef = react.useRef(
9
+ void 0
10
+ );
9
11
  const currentState = stateRef.current;
10
- const shouldRemount = currentState === void 0 || currentState.logic !== logic || !Object.is(currentState.props, props);
12
+ const shouldRemount = currentState === void 0 || currentState.molecule !== molecule || !Object.is(currentState.props, props);
11
13
  if (shouldRemount) {
12
14
  if (currentState !== void 0) {
13
15
  currentState.pendingDisposeToken = null;
14
- core.cleanupLogic(currentState.instance);
16
+ core.disposeMolecule(currentState.instance);
15
17
  stateRef.current = void 0;
16
18
  }
17
- const logicArgs = props === void 0 ? [] : [props];
19
+ const moleculeArgs = props === void 0 ? [] : [props];
18
20
  stateRef.current = {
19
- instance: core.mountLogic(logic, ...logicArgs),
20
- logic,
21
+ instance: molecule(...moleculeArgs),
22
+ molecule,
21
23
  props,
22
24
  subscribers: 0,
23
25
  disposed: false,
@@ -26,7 +28,9 @@ function useLogic(logic, ...args) {
26
28
  }
27
29
  const state = stateRef.current;
28
30
  if (state === void 0) {
29
- throw new Error("useLogic failed to mount the requested logic instance.");
31
+ throw new Error(
32
+ "useMolcule failed to mount the requested molecule instance."
33
+ );
30
34
  }
31
35
  const instance = state.instance;
32
36
  react.useEffect(() => {
@@ -42,7 +46,7 @@ function useLogic(logic, ...args) {
42
46
  return () => {
43
47
  const latest = stateRef.current;
44
48
  if (latest === void 0 || latest.instance !== instance) {
45
- core.cleanupLogic(instance);
49
+ core.disposeMolecule(instance);
46
50
  return;
47
51
  }
48
52
  latest.subscribers -= 1;
@@ -60,7 +64,7 @@ function useLogic(logic, ...args) {
60
64
  current.disposed = true;
61
65
  current.pendingDisposeToken = null;
62
66
  stateRef.current = void 0;
63
- core.cleanupLogic(instance);
67
+ core.disposeMolecule(instance);
64
68
  });
65
69
  }
66
70
  };
@@ -102,6 +106,6 @@ function useDeepSignal(source) {
102
106
 
103
107
  exports.useComputed = useComputed;
104
108
  exports.useDeepSignal = useDeepSignal;
105
- exports.useLogic = useLogic;
109
+ exports.useMolcule = useMolcule;
106
110
  exports.useSignal = useSignal;
107
111
  exports.useSnapshot = useSnapshot;
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- import { LogicFunction, LogicArgs, LogicInstance, Signal, ReadonlySignal, Computed, DeepSignal, SnapshotHandler } from '@sigrea/core';
1
+ import { MoleculeFactory, MoleculeArgs, MoleculeInstance, Signal, ReadonlySignal, Computed, DeepSignal, SnapshotHandler } from '@sigrea/core';
2
2
 
3
- declare function useLogic<TReturn extends object, TProps = void>(logic: LogicFunction<TReturn, TProps>, ...args: LogicArgs<TProps>): LogicInstance<TReturn>;
3
+ declare function useMolcule<TReturn extends object, TProps = void>(molecule: MoleculeFactory<TReturn, TProps>, ...args: MoleculeArgs<TProps>): MoleculeInstance<TReturn>;
4
4
 
5
5
  type ReadableSignal<T> = Signal<T> | ReadonlySignal<T>;
6
6
  declare function useSignal<T>(source: ReadableSignal<T>): T;
@@ -11,4 +11,4 @@ declare function useDeepSignal<T extends object>(source: DeepSignal<T>): T;
11
11
 
12
12
  declare function useSnapshot<T>(handler: SnapshotHandler<T>): T;
13
13
 
14
- export { useComputed, useDeepSignal, useLogic, useSignal, useSnapshot };
14
+ export { useComputed, useDeepSignal, useMolcule, useSignal, useSnapshot };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { LogicFunction, LogicArgs, LogicInstance, Signal, ReadonlySignal, Computed, DeepSignal, SnapshotHandler } from '@sigrea/core';
1
+ import { MoleculeFactory, MoleculeArgs, MoleculeInstance, Signal, ReadonlySignal, Computed, DeepSignal, SnapshotHandler } from '@sigrea/core';
2
2
 
3
- declare function useLogic<TReturn extends object, TProps = void>(logic: LogicFunction<TReturn, TProps>, ...args: LogicArgs<TProps>): LogicInstance<TReturn>;
3
+ declare function useMolcule<TReturn extends object, TProps = void>(molecule: MoleculeFactory<TReturn, TProps>, ...args: MoleculeArgs<TProps>): MoleculeInstance<TReturn>;
4
4
 
5
5
  type ReadableSignal<T> = Signal<T> | ReadonlySignal<T>;
6
6
  declare function useSignal<T>(source: ReadableSignal<T>): T;
@@ -11,4 +11,4 @@ declare function useDeepSignal<T extends object>(source: DeepSignal<T>): T;
11
11
 
12
12
  declare function useSnapshot<T>(handler: SnapshotHandler<T>): T;
13
13
 
14
- export { useComputed, useDeepSignal, useLogic, useSignal, useSnapshot };
14
+ export { useComputed, useDeepSignal, useMolcule, useSignal, useSnapshot };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { LogicFunction, LogicArgs, LogicInstance, Signal, ReadonlySignal, Computed, DeepSignal, SnapshotHandler } from '@sigrea/core';
1
+ import { MoleculeFactory, MoleculeArgs, MoleculeInstance, Signal, ReadonlySignal, Computed, DeepSignal, SnapshotHandler } from '@sigrea/core';
2
2
 
3
- declare function useLogic<TReturn extends object, TProps = void>(logic: LogicFunction<TReturn, TProps>, ...args: LogicArgs<TProps>): LogicInstance<TReturn>;
3
+ declare function useMolcule<TReturn extends object, TProps = void>(molecule: MoleculeFactory<TReturn, TProps>, ...args: MoleculeArgs<TProps>): MoleculeInstance<TReturn>;
4
4
 
5
5
  type ReadableSignal<T> = Signal<T> | ReadonlySignal<T>;
6
6
  declare function useSignal<T>(source: ReadableSignal<T>): T;
@@ -11,4 +11,4 @@ declare function useDeepSignal<T extends object>(source: DeepSignal<T>): T;
11
11
 
12
12
  declare function useSnapshot<T>(handler: SnapshotHandler<T>): T;
13
13
 
14
- export { useComputed, useDeepSignal, useLogic, useSignal, useSnapshot };
14
+ export { useComputed, useDeepSignal, useMolcule, useSignal, useSnapshot };
package/dist/index.mjs CHANGED
@@ -1,21 +1,23 @@
1
1
  import { useRef, useEffect, useCallback, useSyncExternalStore, useMemo } from 'react';
2
- import { cleanupLogic, mountLogic, createSignalHandler, createComputedHandler, createDeepSignalHandler } from '@sigrea/core';
2
+ import { disposeMolecule, createSignalHandler, createComputedHandler, createDeepSignalHandler } from '@sigrea/core';
3
3
 
4
- function useLogic(logic, ...args) {
4
+ function useMolcule(molecule, ...args) {
5
5
  const props = args.length === 0 ? void 0 : args[0];
6
- const stateRef = useRef(void 0);
6
+ const stateRef = useRef(
7
+ void 0
8
+ );
7
9
  const currentState = stateRef.current;
8
- const shouldRemount = currentState === void 0 || currentState.logic !== logic || !Object.is(currentState.props, props);
10
+ const shouldRemount = currentState === void 0 || currentState.molecule !== molecule || !Object.is(currentState.props, props);
9
11
  if (shouldRemount) {
10
12
  if (currentState !== void 0) {
11
13
  currentState.pendingDisposeToken = null;
12
- cleanupLogic(currentState.instance);
14
+ disposeMolecule(currentState.instance);
13
15
  stateRef.current = void 0;
14
16
  }
15
- const logicArgs = props === void 0 ? [] : [props];
17
+ const moleculeArgs = props === void 0 ? [] : [props];
16
18
  stateRef.current = {
17
- instance: mountLogic(logic, ...logicArgs),
18
- logic,
19
+ instance: molecule(...moleculeArgs),
20
+ molecule,
19
21
  props,
20
22
  subscribers: 0,
21
23
  disposed: false,
@@ -24,7 +26,9 @@ function useLogic(logic, ...args) {
24
26
  }
25
27
  const state = stateRef.current;
26
28
  if (state === void 0) {
27
- throw new Error("useLogic failed to mount the requested logic instance.");
29
+ throw new Error(
30
+ "useMolcule failed to mount the requested molecule instance."
31
+ );
28
32
  }
29
33
  const instance = state.instance;
30
34
  useEffect(() => {
@@ -40,7 +44,7 @@ function useLogic(logic, ...args) {
40
44
  return () => {
41
45
  const latest = stateRef.current;
42
46
  if (latest === void 0 || latest.instance !== instance) {
43
- cleanupLogic(instance);
47
+ disposeMolecule(instance);
44
48
  return;
45
49
  }
46
50
  latest.subscribers -= 1;
@@ -58,7 +62,7 @@ function useLogic(logic, ...args) {
58
62
  current.disposed = true;
59
63
  current.pendingDisposeToken = null;
60
64
  stateRef.current = void 0;
61
- cleanupLogic(instance);
65
+ disposeMolecule(instance);
62
66
  });
63
67
  }
64
68
  };
@@ -98,4 +102,4 @@ function useDeepSignal(source) {
98
102
  return useSnapshot(handler);
99
103
  }
100
104
 
101
- export { useComputed, useDeepSignal, useLogic, useSignal, useSnapshot };
105
+ export { useComputed, useDeepSignal, useMolcule, useSignal, useSnapshot };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sigrea/react",
3
- "version": "0.2.1",
4
- "description": "React adapter bindings for Sigrea logic modules.",
3
+ "version": "0.3.1",
4
+ "description": "React adapter bindings for Sigrea molecule modules.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "packageManager": "pnpm@10.0.0",
@@ -36,7 +36,7 @@
36
36
  "signals",
37
37
  "reactivity",
38
38
  "react",
39
- "logic",
39
+ "molecule",
40
40
  "typescript"
41
41
  ],
42
42
  "scripts": {
@@ -49,10 +49,11 @@
49
49
  "test:coverage": "vitest --coverage",
50
50
  "typecheck": "tsc -p tsconfig.json --noEmit",
51
51
  "format": "biome check .",
52
- "format:fix": "biome check --write ."
52
+ "format:fix": "biome check --write .",
53
+ "cicheck": "pnpm test && pnpm typecheck && pnpm format:fix"
53
54
  },
54
55
  "peerDependencies": {
55
- "@sigrea/core": "^0.3.1",
56
+ "@sigrea/core": "^0.4.3",
56
57
  "react": "^18.0.0 || ^19.0.0",
57
58
  "react-dom": "^18.0.0 || ^19.0.0"
58
59
  },