Preact Signals デモ
synstate-preact-signals は synstate の Observable システムと Preact Signals を橋渡しします。これにより、コンポーネントの再レンダリングなしにきめ細かい DOM 更新が可能になります — 影響を受けるテキストノードのみが更新されます。
以下のデモは、実際の synstate-preact-signals API がブラウザ上で動作しています。各 API の詳細は API リファレンス を参照してください。
デモ 1: レンダリング回数 — Hooks vs Signals
Section titled “デモ 1: レンダリング回数 — Hooks vs Signals”ソースコード
import * as Preact from 'preact/hooks';import { createState as createStateBase } from 'synstate';import { useObservableValue } from 'synstate-preact-hooks';import { toSignal } from 'synstate-preact-signals';
// Shared observable state — both panels read from the same sourceconst [ sharedCount$, , { updateState: updateSharedCount, resetState: resetSharedCount },] = createStateBase(0);
const decrement = (): void => { updateSharedCount((n) => n - 1);};
const increment = (): void => { updateSharedCount((n) => n + 1);};
// Signal version of the same stateconst countSignal = toSignal(sharedCount$);
// ---------------------------------------------------------------------------// Combined Demo// ---------------------------------------------------------------------------
export const RenderCountDemo = (): preact.JSX.Element => ( <div> <div style={panelsRowStyle}> <HooksPanel /> <SignalsPanel /> </div> <div style={controlsStyle}> <button style={buttonStyle} type={'button'} onClick={decrement}> {'-1'} </button> <button style={buttonStyle} type={'button'} onClick={increment}> {'+1'} </button> <button style={buttonStyle} type={'button'} onClick={resetSharedCount as () => void} > {'Reset'} </button> </div> </div>);
// ---------------------------------------------------------------------------// Hooks Panel — re-renders the entire component on every change// ---------------------------------------------------------------------------
const HooksPanel = (): preact.JSX.Element => { const count = useObservableValue(sharedCount$);
// Count renders via a ref that increments on every function body execution const renderCountRef = Preact.useRef(0);
renderCountRef.current += 1;
return ( <div style={panelStyle}> <div style={panelTitleStyle}>{'synstate-preact-hooks'}</div> <div style={valueStyle}>{count}</div> <div style={renderCountStyle}> {'Render count: '} <strong>{renderCountRef.current}</strong> </div> </div> );};
// ---------------------------------------------------------------------------// Signals Panel — renders once, only the Signal text nodes update// ---------------------------------------------------------------------------
const SignalsPanel = (): preact.JSX.Element => { // Count renders — this should stay at 1 const renderCountRef = Preact.useRef(0);
renderCountRef.current += 1;
return ( <div style={panelStyle}> <div style={panelTitleStyle}>{'synstate-preact-signals'}</div> <div style={valueStyle}>{countSignal}</div> <div style={renderCountStyle}> {'Render count: '} <strong>{renderCountRef.current}</strong> </div> </div> );};ボタンをクリックしてレンダリング回数を確認してください:
- Hooks パネル —
useObservableValueがコンポーネントの再レンダリングをトリガーするため、状態が変わるたびにレンダリング回数が増加します。 - Signals パネル —
toSignalが返す Preact Signal は DOM テキストノードのみを更新し、コンポーネントの再レンダリングサイクルをバイパスするため、レンダリング回数は 1 のままです。
これが Preact Signals 連携の主な利点です。子要素が多い、JSX が複雑などのコストの高いレンダーツリーを持つコンポーネントは、不要な仮想 DOM 差分計算のスキップの恩恵を受けます。
デモ 2: カウンター — createState と Signals
Section titled “デモ 2: カウンター — createState と Signals”ソースコード
import { map } from 'synstate';import { createBooleanState, createState, toSignal,} from 'synstate-preact-signals';
// State: counterconst [ countSignal, _setCount, { state: count$, updateState, resetState, getSnapshot },] = createState(0);
const decrement = (): void => { updateState((n) => n - 1);};
const increment = (): void => { updateState((n) => n + 1);};
// Derived: doubled value via map + toSignalconst doubledSignal = toSignal(count$.pipe(map((n) => n * 2)));
// State: visibility toggle via createBooleanStateconst [isVisibleSignal, { toggle: toggleVisibility }] = createBooleanState(true);
export const CounterDemo = (): preact.JSX.Element => ( <div> <div style={rowStyle}> <span style={labelStyle}>{'Count:'}</span> <span style={valueStyle}>{countSignal}</span> </div> <div style={rowStyle}> <span style={labelStyle}>{'Doubled (map + toSignal):'}</span> <span style={valueStyle}>{doubledSignal}</span> </div> <div style={rowStyle}> <span style={labelStyle}>{'Visible (createBooleanState):'}</span> <span style={valueStyle}> {isVisibleSignal.value ? 'Yes' : 'No'} </span> </div> {isVisibleSignal.value ? ( <div style={boxStyle}> {'This box is controlled by createBooleanState.'} </div> ) : undefined} <div style={controlsStyle}> <button style={buttonStyle} type={'button'} onClick={decrement}> {'-1'} </button> <button style={buttonStyle} type={'button'} onClick={increment}> {'+1'} </button> <button style={buttonStyle} type={'button'} onClick={resetState}> {'Reset'} </button> <button style={buttonStyle} type={'button'} onClick={toggleVisibility} > {'Toggle Visibility'} </button> <button style={buttonStyle} type={'button'} onClick={() => { // eslint-disable-next-line no-alert alert(`getSnapshot() = ${String(getSnapshot())}`); }} > {'getSnapshot()'} </button> </div> </div>);Signal を返すラッパーの動作確認:
createState— フックの代わりにReadonlySignalを返す。カウンター値はその場で更新される。map+toSignal— Observable から新しい Signal を導出。「Doubled」の値はcount$.pipe(map(n => n * 2))で計算され、Signal に変換される。createBooleanState— 真偽値トグルの便利なラッパー。表示ボックスがsetTrue、setFalse、toggleを実演。getSnapshot()— 現在の値を同期的に読み取る。イベントハンドラ内での使用に便利。
デモ 3: Signal ↔ Observable ブリッジ
Section titled “デモ 3: Signal ↔ Observable ブリッジ”ソースコード
import { signal } from '@preact/signals';import { debounce, map } from 'synstate';import { fromSignal, toSignal } from 'synstate-preact-signals';
// Raw input signal (Preact Signal — updates immediately on every keystroke)const inputSignal = signal('');
// Bridge: Signal → Observable (via fromSignal)const [input$, _dispose] = fromSignal(inputSignal);
// Apply synstate operators on the observable sideconst debouncedInput$ = input$.pipe(debounce(500));
// Bridge back: Observable → Signal (via toSignal)const debouncedSignal = toSignal(debouncedInput$, '');
// Count how many times the debounced value has changedconst updateCount$ = debouncedInput$.pipe(map((_, i) => i + 1));
const updateCountSignal = toSignal(updateCount$);
// Track input length as a derived observableconst inputLength$ = input$.pipe(map((s) => s.length));
const inputLengthSignal = toSignal(inputLength$);
// ---------------------------------------------------------------------------// Component// ---------------------------------------------------------------------------
export const SignalBridgeDemo = (): preact.JSX.Element => ( <div> <div style={inputRowStyle}> <label style={labelStyle}> {'Input (Preact Signal): '} <input style={inputStyle} type={'text'} placeholder={'Type something...'} value={inputSignal} onInput={(e) => { // eslint-disable-next-line functional/immutable-data, total-functions/no-unsafe-type-assertion inputSignal.value = ( e.target as HTMLInputElement ).value; }} /> </label> </div>
<div style={gridStyle}> <div style={cellStyle}> <div style={cellLabelStyle}>{'Raw signal value'}</div> <div style={cellValueStyle}>{inputSignal}</div> </div> <div style={cellStyle}> <div style={cellLabelStyle}>{'Debounced (500ms)'}</div> <div style={cellValueStyle}>{debouncedSignal}</div> </div> <div style={cellStyle}> <div style={cellLabelStyle}>{'Input length (map)'}</div> <div style={cellValueStyle}>{inputLengthSignal}</div> </div> <div style={cellStyle}> <div style={cellLabelStyle}> {'Debounce update count (map)'} </div> <div style={cellValueStyle}>{updateCountSignal}</div> </div> </div>
<div style={pipelineStyle}> <code style={codeStyle}> {'Signal → fromSignal → debounce(500) → toSignal'} </code> <br /> <code style={codeStyle}> {'Signal → fromSignal → map(s => s.length) → toSignal'} </code> <br /> <code style={codeStyle}> {'Signal → fromSignal → debounce(500) → map(index) → toSignal'} </code> </div> </div>);双方向変換のデモ:
fromSignal— Preact Signal を synstate の Observable に変換し、オペレーターチェーン(debounce、map等)を適用可能にする。toSignal— 加工された Observable を Signal に変換してレンダリングに使用。- 入力は即座に更新(raw Signal)されますが、デバウンスされた値は500ms の入力停止後に更新されます。
- 更新回数と入力文字数はどちらも
mapを使用 — 更新回数は第2引数(emission index)を利用しmap((_, i) => i + 1)で実装。