diff --git a/CHANGELOG.md b/CHANGELOG.md index 66b6b1e..6275ce3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,9 @@ ### 🩹 Fixes -- Dispose logic instances synchronously ([1f202e9](https://github.com/sigrea/react/commit/1f202e9)) -- Defer logic disposal until post-rerender ([49cacc7](https://github.com/sigrea/react/commit/49cacc7)) -- Defer useLogic disposal until rerender completes ([#6](https://github.com/sigrea/react/pull/6)) +- Dispose molcule instances synchronously ([1f202e9](https://github.com/sigrea/react/commit/1f202e9)) +- Defer molcule disposal until post-rerender ([49cacc7](https://github.com/sigrea/react/commit/49cacc7)) +- Defer useMolcule disposal until rerender completes ([#6](https://github.com/sigrea/react/pull/6)) ### 📖 Documentation @@ -39,11 +39,11 @@ ### ✅ Tests -- Cover useLogic rerender and strict mode replay ([5aa1c34](https://github.com/sigrea/react/commit/5aa1c34)) +- Cover useMolcule rerender and strict mode replay ([5aa1c34](https://github.com/sigrea/react/commit/5aa1c34)) ### 🎨 Styles -- Format strict-mode useLogic test ([ec918fb](https://github.com/sigrea/react/commit/ec918fb)) +- Format strict-mode useMolcule test ([ec918fb](https://github.com/sigrea/react/commit/ec918fb)) ### ❤️ Contributors @@ -54,4 +54,4 @@ ### Minor Changes -- e7fa75e: Introduce the initial React adapter hooks for Sigrea logic and signals, including playground scaffolding for manual verification. +- e7fa75e: Introduce the initial React adapter hooks for Sigrea molcules and signals, including playground scaffolding for manual verification. diff --git a/README.md b/README.md index d5a8a3b..32ddfb8 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,24 @@ # @sigrea/react -`@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. +`@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. - **Signal subscriptions.** `useSignal` subscribes to signals and computed values, triggering re-renders when they change. - **Computed subscriptions.** `useComputed` subscribes to computed values and memoizes them per component instance. - **Deep signal subscriptions.** `useDeepSignal` subscribes to deep signal objects and exposes them for direct mutation. -- **Logic lifecycles.** `useLogic` mounts logic factories and binds their lifecycles to React components. +- **Molecule lifecycles.** `useMolcule` mounts molecule factories and binds their lifecycles to React components. ## Table of Contents - [Install](#install) - [Quick Start](#quick-start) - [Consume a Signal](#consume-a-signal) - - [Bridge Framework-Agnostic Logic](#bridge-framework-agnostic-logic) + - [Bridge Framework-Agnostic Molecules](#bridge-framework-agnostic-molecules) - [Work with Deep Signals](#work-with-deep-signals) - [API Reference](#api-reference) - [useSignal](#usesignal) - [useComputed](#usecomputed) - [useDeepSignal](#usedeepsignal) - - [useLogic](#uselogic) + - [useMolcule](#usemolcule) - [Testing](#testing) - [Development](#development) - [License](#license) @@ -47,13 +47,13 @@ export function CounterLabel() { } ``` -### Bridge Framework-Agnostic Logic +### Bridge Framework-Agnostic Molecules ```tsx -import { defineLogic, signal } from "@sigrea/core"; -import { useLogic, useSignal } from "@sigrea/react"; +import { molecule, signal } from "@sigrea/core"; +import { useMolcule, useSignal } from "@sigrea/react"; -const CounterLogic = defineLogic<{ initialCount: number }>()((props) => { +const CounterMolecule = molecule((props: { initialCount: number }) => { const count = signal(props.initialCount); const increment = () => { @@ -68,7 +68,7 @@ const CounterLogic = defineLogic<{ initialCount: number }>()((props) => { }); export function Counter(props: { initialCount: number }) { - const counter = useLogic(CounterLogic, props); + const counter = useMolcule(CounterMolcule, props); const value = useSignal(counter.count); return ( @@ -132,27 +132,24 @@ function useDeepSignal(signal: DeepSignal): T 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. -### useLogic +### useMolcule ```tsx -function useLogic( - logic: LogicFunction, +function useMolcule( + molecule: MoleculeFactory, props?: TProps ): TReturn ``` -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. +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. ## Testing ```tsx // tests/Counter.test.tsx import { render, screen, fireEvent } from "@testing-library/react"; -import { cleanupLogics } from "@sigrea/core"; import { Counter } from "../components/Counter"; -afterEach(() => cleanupLogics()); - it("increments and displays the updated count", () => { render(); diff --git a/package.json b/package.json index 67ba097..7dad9ed 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@sigrea/react", "version": "0.2.1", - "description": "React adapter bindings for Sigrea logic modules.", + "description": "React adapter bindings for Sigrea molecule modules.", "license": "MIT", "type": "module", "packageManager": "pnpm@10.0.0", @@ -30,7 +30,7 @@ "main": "./dist/index.cjs", "types": "./dist/index.d.ts", "files": ["dist"], - "keywords": ["signals", "reactivity", "react", "logic", "typescript"], + "keywords": ["signals", "reactivity", "react", "molecule", "typescript"], "scripts": { "dev": "vite --config playground/vite.config.ts", "build": "unbuild", @@ -44,7 +44,7 @@ "format:fix": "biome check --write ." }, "peerDependencies": { - "@sigrea/core": "^0.3.1", + "@sigrea/core": "^0.4.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, diff --git a/packages/__tests__/useLogic.strict-mode.test.ts b/packages/__tests__/useMolcule.strict-mode.test.ts similarity index 68% rename from packages/__tests__/useLogic.strict-mode.test.ts rename to packages/__tests__/useMolcule.strict-mode.test.ts index fa504fe..d54d319 100644 --- a/packages/__tests__/useLogic.strict-mode.test.ts +++ b/packages/__tests__/useMolcule.strict-mode.test.ts @@ -1,12 +1,12 @@ import { StrictMode, createElement } from "react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { cleanupLogics, defineLogic, onUnmount } from "@sigrea/core"; +import { disposeTrackedMolecules, molecule, onUnmount } from "@sigrea/core"; -import { useLogic } from "../useLogic"; +import { useMolcule } from "../useMolcule"; import { createTestRoot, flushMicrotasks } from "./testUtils"; -describe("useLogic in StrictMode", () => { +describe("useMolcule in StrictMode", () => { let root: ReturnType; beforeEach(() => { @@ -15,18 +15,18 @@ describe("useLogic in StrictMode", () => { afterEach(async () => { await root.unmount(); - cleanupLogics(); + disposeTrackedMolecules(); }); - it("keeps the logic instance alive across StrictMode effect replays", async () => { + it("keeps the molecule instance alive across StrictMode effect replays", async () => { const cleanup = vi.fn(); - const logic = defineLogic()((value) => { + const counterMolcule = molecule((value: number) => { onUnmount(() => cleanup(value)); return { value }; }); function TestComponent() { - useLogic(logic, 1); + useMolcule(counterMolcule, 1); return null; } diff --git a/packages/__tests__/useLogic.test.ts b/packages/__tests__/useMolcule.test.ts similarity index 79% rename from packages/__tests__/useLogic.test.ts rename to packages/__tests__/useMolcule.test.ts index b9cc2a7..11d090c 100644 --- a/packages/__tests__/useLogic.test.ts +++ b/packages/__tests__/useMolcule.test.ts @@ -2,16 +2,16 @@ import { createElement } from "react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { - type LogicInstance, - cleanupLogics, - defineLogic, + type MoleculeInstance, + disposeTrackedMolecules, + molecule, onUnmount, } from "@sigrea/core"; -import { useLogic } from "../useLogic"; +import { useMolcule } from "../useMolcule"; import { createTestRoot, flushMicrotasks } from "./testUtils"; -describe("useLogic", () => { +describe("useMolcule", () => { let root: ReturnType; beforeEach(() => { @@ -20,20 +20,20 @@ describe("useLogic", () => { afterEach(async () => { await root.unmount(); - cleanupLogics(); + disposeTrackedMolecules(); }); it("does not dispose when re-rendered with identical props", async () => { const cleanup = vi.fn(); - const logic = defineLogic()((value) => { + const counterMolcule = molecule((value: number) => { onUnmount(() => cleanup(value)); return { value }; }); - const observed: Array> = []; + const observed: Array> = []; function TestComponent({ value }: { value: number }) { - const instance = useLogic(logic, value); + const instance = useMolcule(counterMolcule, value); observed.push(instance); return null; } @@ -54,17 +54,17 @@ describe("useLogic", () => { expect(cleanup).toHaveBeenCalledWith(1); }); - it("mounts logic and cleans up on unmount", async () => { + it("mounts molecule and cleans up on unmount", async () => { const cleanup = vi.fn(); - const makeLogic = defineLogic()((value) => { + const makeMolcule = molecule((value: number) => { onUnmount(() => cleanup(value)); return { value }; }); - const observed: Array> = []; + const observed: Array> = []; function TestComponent() { - const instance = useLogic(makeLogic, 1); + const instance = useMolcule(makeMolcule, 1); observed.push(instance); return null; } @@ -86,14 +86,14 @@ describe("useLogic", () => { const mounts = vi.fn(); const cleanups = vi.fn(); - const logic = defineLogic()((value) => { + const counterMolcule = molecule((value: number) => { mounts(value); onUnmount(() => cleanups(value)); return {}; }); function TestComponent({ value }: { value: number }) { - useLogic(logic, value); + useMolcule(counterMolcule, value); return null; } diff --git a/packages/index.ts b/packages/index.ts index 7d00578..d6dc942 100644 --- a/packages/index.ts +++ b/packages/index.ts @@ -1,10 +1,10 @@ /** * ================================================== - * logic + * molecule * ================================================== */ -export { useLogic } from "./useLogic"; +export { useMolcule } from "./useMolcule"; /** * ================================================== diff --git a/packages/useLogic.ts b/packages/useMolcule.ts similarity index 65% rename from packages/useLogic.ts rename to packages/useMolcule.ts index a129c9c..9f72993 100644 --- a/packages/useLogic.ts +++ b/packages/useMolcule.ts @@ -1,45 +1,51 @@ import { useEffect, useRef } from "react"; -import type { LogicArgs, LogicFunction, LogicInstance } from "@sigrea/core"; -import { cleanupLogic, mountLogic } from "@sigrea/core"; - -interface LogicState { - instance: LogicInstance; - logic: LogicFunction; +import type { + MoleculeArgs, + MoleculeFactory, + MoleculeInstance, +} from "@sigrea/core"; +import { disposeMolecule } from "@sigrea/core"; + +interface MoleculeState { + instance: MoleculeInstance; + molecule: MoleculeFactory; props: TProps | undefined; subscribers: number; disposed: boolean; pendingDisposeToken: symbol | null; } -export function useLogic( - logic: LogicFunction, - ...args: LogicArgs -): LogicInstance { +export function useMolcule( + molecule: MoleculeFactory, + ...args: MoleculeArgs +): MoleculeInstance { const props = args.length === 0 ? undefined : (args[0] as TProps | undefined); - const stateRef = useRef | undefined>(undefined); + const stateRef = useRef | undefined>( + undefined, + ); const currentState = stateRef.current; const shouldRemount = currentState === undefined || - currentState.logic !== logic || + currentState.molecule !== molecule || !Object.is(currentState.props, props); if (shouldRemount) { if (currentState !== undefined) { currentState.pendingDisposeToken = null; - cleanupLogic(currentState.instance); + disposeMolecule(currentState.instance); stateRef.current = undefined; } - const logicArgs = + const moleculeArgs = props === undefined - ? ([] as LogicArgs) - : ([props] as unknown as LogicArgs); + ? ([] as MoleculeArgs) + : ([props] as unknown as MoleculeArgs); stateRef.current = { - instance: mountLogic(logic, ...logicArgs), - logic, + instance: molecule(...moleculeArgs), + molecule, props, subscribers: 0, disposed: false, @@ -49,7 +55,9 @@ export function useLogic( const state = stateRef.current; if (state === undefined) { - throw new Error("useLogic failed to mount the requested logic instance."); + throw new Error( + "useMolcule failed to mount the requested molecule instance.", + ); } const instance = state.instance; @@ -69,7 +77,7 @@ export function useLogic( return () => { const latest = stateRef.current; if (latest === undefined || latest.instance !== instance) { - cleanupLogic(instance); + disposeMolecule(instance); return; } @@ -97,7 +105,7 @@ export function useLogic( current.disposed = true; current.pendingDisposeToken = null; stateRef.current = undefined; - cleanupLogic(instance); + disposeMolecule(instance); }); } }; diff --git a/playground/src/Counter.tsx b/playground/src/Counter.tsx index 206e574..fb00f60 100644 --- a/playground/src/Counter.tsx +++ b/playground/src/Counter.tsx @@ -1,16 +1,16 @@ import { useMemo } from "react"; -import { useLogic, useSignal } from "@sigrea/react"; -import { CounterLogic, type CounterProps } from "./CounterLogic"; +import { useMolcule, useSignal } from "@sigrea/react"; +import { CounterMolecule, type CounterProps } from "./CounterMolecule"; export function Counter(props: CounterProps) { const { initialCount, step } = props; - const logicProps = useMemo( + const moleculeProps = useMemo( () => ({ initialCount, step }), [initialCount, step], ); - const counter = useLogic(CounterLogic, logicProps); + const counter = useMolcule(CounterMolecule, moleculeProps); const count = useSignal(counter.count); return ( diff --git a/playground/src/CounterLogic.ts b/playground/src/CounterMolecule.ts similarity index 74% rename from playground/src/CounterLogic.ts rename to playground/src/CounterMolecule.ts index 97aa93c..ad78dd0 100644 --- a/playground/src/CounterLogic.ts +++ b/playground/src/CounterMolecule.ts @@ -1,13 +1,12 @@ -import { defineLogic, onMount, onUnmount, signal, watch } from "@sigrea/core"; +import { molecule, onMount, onUnmount, signal, watch } from "@sigrea/core"; export interface CounterProps { initialCount?: number; step?: number; } -const createCounterLogic = defineLogic(); - -export const CounterLogic = createCounterLogic(({ initialCount, step }) => { +export const CounterMolecule = molecule((props: CounterProps) => { + const { initialCount, step } = props; const initial = initialCount ?? 0; const incrementStep = step ?? 1; const count = signal(initial); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08bb80f..877cb7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@sigrea/core': - specifier: ^0.3.1 - version: 0.3.1 + specifier: ^0.4.0 + version: 0.4.0 devDependencies: '@biomejs/biome': specifier: 1.9.4 @@ -724,8 +724,8 @@ packages: cpu: [x64] os: [win32] - '@sigrea/core@0.3.1': - resolution: {integrity: sha512-pT5QOKRXx2zR4XD5U/9O+0wkb5pvc7DnKHo1E+Sr/Q6+XM/YOpt+IQh9ObFSpX6FzM6IVL5vzq1I2FYtyOS73g==} + '@sigrea/core@0.4.0': + resolution: {integrity: sha512-5/ppPY8o7/pYsxpxdqf+x+kuwg08Y9V3F/BC8h+azjHC+ctmmXtznTSsGASpeD4jG5KgEbc7SfbA8eFJnvp9xw==} engines: {node: '>=20'} '@types/babel__core@7.20.5': @@ -813,8 +813,8 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} - alien-signals@3.0.3: - resolution: {integrity: sha512-2JXjom6R7ZwrISpUphLhf4htUq1aKRCennTJ6u9kFfr3sLmC9+I4CxxVi+McoFnIg+p1HnVrfLT/iCt4Dlz//Q==} + alien-signals@3.1.1: + resolution: {integrity: sha512-ogkIWbVrLwKtHY6oOAXaYkAxP+cTH7V5FZ5+Tm4NZFd8VDZ6uNMDrfzqctTZ42eTMCSR3ne3otpcxmqSnFfPYA==} ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} @@ -2569,9 +2569,9 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.52.5': optional: true - '@sigrea/core@0.3.1': + '@sigrea/core@0.4.0': dependencies: - alien-signals: 3.0.3 + alien-signals: 3.1.1 '@types/babel__core@7.20.5': dependencies: @@ -2690,7 +2690,7 @@ snapshots: agent-base@7.1.4: {} - alien-signals@3.0.3: {} + alien-signals@3.1.1: {} ansi-regex@5.0.1: {}