diff --git a/README.md b/README.md
index 7469157..de7e50e 100644
--- a/README.md
+++ b/README.md
@@ -1,28 +1,11 @@
# @sigrea/react
-`@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.
-
-- **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.
-- **Molecule lifecycles.** `useMolecule` 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 Molecules](#bridge-framework-agnostic-molecules)
- - [Work with Deep Signals](#work-with-deep-signals)
-- [API Reference](#api-reference)
- - [useSignal](#usesignal)
- - [useComputed](#usecomputed)
- - [useDeepSignal](#usedeepsignal)
- - [useMolecule](#usemolecule)
-- [Testing](#testing)
-- [Handling Scope Cleanup Errors](#handling-scope-cleanup-errors)
-- [Development](#development)
-- [License](#license)
+React hooks for [@sigrea/core](https://www.npmjs.com/package/@sigrea/core).
+Use this package when a React component needs to mount a molecule, read a
+Sigrea signal, or subscribe to a deep signal object.
+
+`@sigrea/react` does not replace React state. It subscribes components to
+Sigrea signals and mounts molecules with the component lifecycle.
## Install
@@ -30,28 +13,151 @@
npm install @sigrea/react @sigrea/core react react-dom
```
-Install `@sigrea/use` as well when shared molecules use utilities such as
-`createEvents`.
-
Requires React 18+ and Node.js 24 or later.
+Install `@sigrea/use` too if your shared molecules use helpers from that
+package, such as `createEvents`.
+
## Quick Start
-### Consume a Signal
+Define state in a molecule. Mount that molecule in a component with
+`useMolecule()`, then read returned signals with `useSignal()`.
+
+```tsx
+import { molecule, readonly, signal } from "@sigrea/core";
+import { useMolecule, useSignal } from "@sigrea/react";
+
+const CounterMolecule = molecule(() => {
+ const count = signal(0);
+
+ const increment = () => {
+ count.value++;
+ };
+
+ return {
+ count: readonly(count),
+ increment,
+ };
+});
+
+export function Counter() {
+ const counter = useMolecule(CounterMolecule);
+ const count = useSignal(counter.count);
+
+ return (
+
+ );
+}
+```
+
+Use `useComputed()` when the source is known to be a `computed()` value and you
+want TypeScript to enforce that. `useSignal()` also works with computed values.
+
+## Molecules With Props
+
+Pass a props object when the molecule only needs the initial values. React keeps
+the same molecule instance while the factory stays the same, so object props are
+not resynced on each render.
+
+```tsx
+import { molecule, readonly, signal } from "@sigrea/core";
+import { useMolecule, useSignal } from "@sigrea/react";
+
+const CounterMolecule = molecule((props: { initialCount: number }) => {
+ const count = signal(props.initialCount);
+
+ const reset = () => {
+ count.value = props.initialCount;
+ };
+
+ const increment = () => {
+ count.value++;
+ };
+
+ return {
+ count: readonly(count),
+ increment,
+ reset,
+ };
+});
+
+function Counter({ initialCount }: { initialCount: number }) {
+ const counter = useMolecule(CounterMolecule, { initialCount });
+ const count = useSignal(counter.count);
+
+ return (
+ <>
+ Count: {count}
+
+
+ >
+ );
+}
+```
+
+Pass a props getter when the molecule must keep reading updated React values.
+The dependency list is required and follows normal React rules.
```tsx
-import { signal } from "@sigrea/core";
-import { useSignal } from "@sigrea/react";
+import { computed, molecule } from "@sigrea/core";
+import { useMolecule, useSignal } from "@sigrea/react";
+
+const LabelMolecule = molecule((props: { label: string }) => {
+ return {
+ label: computed(() => props.label.trim()),
+ };
+});
+
+function Label({ label }: { label: string }) {
+ const model = useMolecule(LabelMolecule, () => ({ label }), [label]);
+ const text = useSignal(model.label);
+
+ return {text};
+}
+```
+
+Inside a molecule, read props as `props.name`. Destructuring copies the current
+value and loses reactivity.
-const count = signal(0);
+## Deep Signals
-export function CounterLabel() {
- const value = useSignal(count);
- return {value};
+Use `useDeepSignal()` when a component reads or mutates a `deepSignal()` object.
+The returned object keeps its identity, and deep mutations trigger a re-render.
+
+```tsx
+import { deepSignal } from "@sigrea/core";
+import { useDeepSignal } from "@sigrea/react";
+
+const profile = deepSignal({ name: "Mendako" });
+
+export function ProfileForm() {
+ const state = useDeepSignal(profile);
+
+ return (
+
+ );
}
```
-### Bridge Framework-Agnostic Molecules
+## Controlled Values
+
+For controlled UI, keep the value in a controller molecule. A child molecule
+calls `send("update:open", next)` when it wants the value to change.
+`@sigrea/use` provides `createEvents()` for this pattern.
```tsx
import {
@@ -62,8 +168,8 @@ import {
signal,
toSignal,
} from "@sigrea/core";
-import { useMolecule, useSignal } from "@sigrea/react";
import { createEvents } from "@sigrea/use";
+import { useMolecule, useSignal } from "@sigrea/react";
type DialogProps = {
open: boolean;
@@ -74,43 +180,28 @@ type DialogEvents = {
"update:open": [next: boolean];
};
-const DialogMolecule = molecule((props) => {
+const DialogMolecule = molecule((props: DialogProps) => {
const { send, on } = createEvents();
const isOpen = toSignal(props, "open");
const isDisabled = computed(() => props.disabled ?? false);
- const emitOpenChange = async (next: boolean) => {
+ const setOpen = async (next: boolean) => {
if (isDisabled.value || isOpen.value === next) {
return;
}
- await send("update:open", next);
- };
-
- const open = () => {
- return emitOpenChange(true);
- };
- const close = () => {
- return emitOpenChange(false);
- };
-
- const toggle = () => {
- return emitOpenChange(!isOpen.value);
+ await send("update:open", next);
};
return {
on,
- open,
- close,
- toggle,
+ toggle: () => setOpen(!isOpen.value),
};
});
const DialogControllerMolecule = molecule(() => {
const isOpen = signal(false);
- const dialog = get(DialogMolecule, () => ({
- open: isOpen.value,
- }));
+ const dialog = get(DialogMolecule, () => ({ open: isOpen.value }));
dialog.on("update:open", (next) => {
isOpen.value = next;
@@ -118,8 +209,6 @@ const DialogControllerMolecule = molecule(() => {
return {
isOpen: readonly(isOpen),
- open: dialog.open,
- close: dialog.close,
toggle: dialog.toggle,
};
});
@@ -129,70 +218,20 @@ export function DialogButton() {
const isOpen = useSignal(dialog.isOpen);
return (
-