3030npm install @sigrea/react @sigrea/core react react-dom
3131```
3232
33+ Install ` @sigrea/use ` as well when shared molecules use utilities such as
34+ ` createEvents ` .
35+
3336Requires React 18+ and Node.js 24 or later.
3437
3538## Quick Start
@@ -51,51 +54,70 @@ export function CounterLabel() {
5154### Bridge Framework-Agnostic Molecules
5255
5356``` tsx
54- import { molecule , readonly , signal } from " @sigrea/core" ;
57+ import {
58+ computed ,
59+ get ,
60+ molecule ,
61+ readonly ,
62+ signal ,
63+ toSignal ,
64+ } from " @sigrea/core" ;
5565import { useMolecule , useSignal } from " @sigrea/react" ;
66+ import { createEvents } from " @sigrea/use" ;
5667
57- type CounterProps = {
58- initialCount : number ;
59- initialStep : number ;
68+ type DialogProps = {
69+ open : boolean ;
70+ disabled ? : boolean ;
6071};
6172
62- const CounterMolecule = molecule (( props : CounterProps ) => {
63- const count = signal ( props . initialCount ) ;
64- const step = signal ( props . initialStep ) ;
73+ type DialogEvents = {
74+ " update:open " : [ open : boolean ] ;
75+ } ;
6576
66- function setStep(next : number ) {
67- step .value = next ;
68- }
77+ const DialogMolecule = molecule <DialogProps >((props ) => {
78+ const { send, on } = createEvents <DialogEvents >();
79+ const open = toSignal (props , " open" );
80+ const disabled = computed (() => props .disabled ?? false );
6981
70- function increment() {
71- count .value += step .value ;
72- }
82+ const requestOpenChange = async (nextOpen : boolean ) => {
83+ if (disabled .value ) {
84+ return ;
85+ }
86+ await send (" update:open" , nextOpen );
87+ };
7388
74- function reset() {
75- count .value = props .initialCount ;
76- }
89+ return {
90+ disabled ,
91+ on ,
92+ open ,
93+ requestOpenChange ,
94+ };
95+ });
96+
97+ const DialogControllerMolecule = molecule (() => {
98+ const open = signal (false );
99+ const dialog = get (DialogMolecule , () => ({
100+ open: open .value ,
101+ }));
102+
103+ dialog .on (" update:open" , (nextOpen ) => {
104+ open .value = nextOpen ;
105+ });
77106
78107 return {
79- count: readonly (count ),
80- step: readonly (step ),
81- setStep ,
82- increment ,
83- reset ,
108+ open: readonly (open ),
109+ requestOpenChange: dialog .requestOpenChange ,
84110 };
85111});
86112
87- export function Counter(props : CounterProps ) {
88- const counter = useMolecule (CounterMolecule , props );
89- const count = useSignal (counter .count );
90- const step = useSignal (counter .step );
113+ export function DialogButton() {
114+ const dialog = useMolecule (DialogControllerMolecule );
115+ const currentOpen = useSignal (dialog .open );
91116
92117 return (
93- <div >
94- <span >{ count } </span >
95- <button onClick = { counter .increment } >Increment</button >
96- <button onClick = { counter .reset } >Reset</button >
97- <button onClick = { () => counter .setStep (step + 1 )} >Step +</button >
98- </div >
118+ <button onClick = { () => dialog .requestOpenChange (! currentOpen )} >
119+ { currentOpen ? " Close" : " Open" }
120+ </button >
99121 );
100122}
101123```
@@ -137,13 +159,16 @@ function useSignal<T>(
137159
138160Subscribes to a signal or computed value and returns its current value . The component re - renders when the source changes .
139161
162+ Unlike the Vue adapter , this hook returns the unwrapped value ` T ` directly rather
163+ than a ref .
164+
140165### useComputed
141166
142167` ` ` tsx
143168function useComputed<T>(source: Computed<T>): T
144169` ` `
145170
146- Subscribes to a computed value and returns its current value . This behaves like ` useSignal(source) ` for computed sources , but keeps the call site explicit when the source is known to be computed .
171+ Subscribes to a computed value and returns its current value . Prefer this over ` useSignal ` when the source is statically known to be ` Computed<T> ` , so type - checking enforces that only computed sources are passed .
147172
148173### useDeepSignal
149174
@@ -156,10 +181,20 @@ Exposes a deep signal object for direct mutation within the component. Updates t
156181### useMolecule
157182
158183` ` ` tsx
159- function useMolecule<TReturn extends object, TProps extends object | void = void>(
184+ function useMolecule<TReturn extends object>(
185+ molecule: MoleculeFactory<TReturn, void>
186+ ): MoleculeInstance<TReturn, void>
187+
188+ function useMolecule<TReturn extends object, TProps extends object>(
189+ molecule: MoleculeFactory<TReturn, TProps>,
190+ props: TProps
191+ ): MoleculeInstance<TReturn, TProps>
192+
193+ function useMolecule<TReturn extends object, TProps extends object>(
160194 molecule: MoleculeFactory<TReturn, TProps>,
161- ...args: MoleculeArgs<TProps>
162- ): MoleculeInstance<TReturn>
195+ props: () => TProps,
196+ deps: DependencyList
197+ ): MoleculeInstance<TReturn, TProps>
163198` ` `
164199
165200Mounts a molecule factory and returns its MoleculeInstance . The molecule ' s scope is bound to the component lifecycle: `onMount` callbacks run after the component mounts, and `onUnmount` callbacks run before it unmounts.
@@ -173,11 +208,38 @@ Molecule lifecycles are bound to React commits for precise timing control:
173208- ` onMount ` , ` watch ` , and ` watchEffect ` registered during setup do not run during server rendering .
174209- After a ** server render ** finishes , the unmounted molecule instance is disposed automatically in a microtask so setup - scope ` onDispose ` cleanups do not leak across requests .
175210
176- This design ensures that ` onMount ` callbacks and ` watch ` effects activate at the right moment —early enough to set up subscriptions before the first paint , yet safely after the component has committed to the DOM .
211+ ` onMount ` , ` watch ` , and ` watchEffect ` run after the component commits . In the
212+ browser , they run before paint .
177213
178214** Props Handling **
179215
180- Props are treated as an initial snapshot . Updating component props does not recreate the molecule instance or update the snapshot ; model dynamic values via signals or explicit molecule methods (for example , ` setStep ` ).
216+ ` useMolecule ` keeps the same molecule instance while the factory stays the same .
217+ Molecules without props , and molecules whose props are all optional , can be
218+ mounted without a props argument . Passing a props object directly creates an
219+ initial snapshot . Passing a props getter requires a React dependency list , such
220+ as ` () => ({ open }) ` with ` [open] ` , and syncs top - level props only after those
221+ dependencies change . This matches React ' s dependency model and avoids resyncing
222+ referential props on every commit .
223+
224+ Inside a molecule , read props as ` props.name ` ; destructuring copies the current
225+ value and loses reactivity .
226+
227+ React components mount the root or controller molecule and use ` useSignal() ` to
228+ read returned signals . Raw molecule events such as ` dialog.on(...) ` belong
229+ inside the molecule graph , not in component bodies . If a UI wrapper needs a
230+ React - controlled API such as ` open ` + ` onOpenChange ` , bridge it at the wrapper
231+ boundary .
232+
233+ ** Client Components and SSR **
234+
235+ ` @sigrea/react ` exports hooks and is intended for Client Components . Do not call
236+ ` useMolecule ` , ` useSignal ` , ` useComputed ` , or ` useDeepSignal ` directly from a
237+ React Server Component .
238+
239+ During server rendering , molecule instances can be created for the render pass ,
240+ but they are not mounted . ` onMount ` , ` watch ` , and ` watchEffect ` registered during
241+ setup do not run on the server . After server rendering completes , unmounted
242+ molecules are disposed in a microtask .
181243
182244## Testing
183245
0 commit comments