Skip to content

Instantly share code, notes, and snippets.

@calloc134
Last active January 14, 2026 07:24
Show Gist options
  • Select an option

  • Save calloc134/6f80a0c941cfe5317120fa1c881f7505 to your computer and use it in GitHub Desktop.

Select an option

Save calloc134/6f80a0c941cfe5317120fa1c881f7505 to your computer and use it in GitHub Desktop.
React の思想をそのまま体現する言語設計・フレームワークがあったらどうなるのか?ChatGPTに相談してみた

TSX-FX / FiberFX 仕様書(ドラフト v0.1)

本書は、あなたの Design 3(TSX-FX + FiberFX) をベースに、指定の変更点(read-set / パス単位 deps、Tx 同期スコープ限定、最小権限 capability+型付き Port、Msg/reducer をオプション化)を取り込み、さらに Design 1/2/4 の「筋の良い部分」を積極的にマージした “破れない” 仕様としてまとめたものです。


0. 用語・前提

用語 意味
Pure Zone 関数コンポーネント本体(= render)。純粋・閉包禁止・外部参照禁止
Fx Zone 副作用が許されるコールバック内部(イベント / ライフサイクル同期)
Snapshot render 入力として渡される不変値(props/state/context の “その時点の写像”)
In<T> Snapshot 型(deep immutable / deep readonly)
Tx state 更新を溜めて 1 回の commit にするためのトランザクション(同期スコープ限定
Capability dom/time/storage 等の外部アクセス権。注入されない限り使えない
Port(型付きポート) UserApi のように「許可された外部操作だけ」を型で列挙した capability(OpenAPI 連携も想定)
read-set use.sync などのコールバックが “読む” In<T>(できればパス単位)集合
deps read-set 由来の依存集合(React の依存配列に相当)。開発者は書かない

1. 設計原理(必ず守るルール)

1.1 2 つの世界を型とスコープで分離

  • Pure Zone(render)

    • 外部システムアクセス禁止(DOM/Network/Time/Random/Storage…)
    • 外部値参照禁止(後述の「閉包禁止」)
    • 入力は In<T> のみ(props/state/context はすべて Snapshot)
    • render 中の更新禁止(Tx を得られない)
  • Fx Zone(副作用コールバック)

    • 命令的・手続き的 OK
    • 外部アクセスは capability 経由のみ
    • state 更新は Tx 経由のみ(同期スコープ限定)

2. 言語仕様(TSX-FX)

2.1 Snapshot 型 In<T>

  • In<T>deep readonly(ネストした配列・オブジェクトまで変更不可)
  • In<T>render 実行中不変(スナップショット)
  • In<T> から得た参照(サブオブジェクト)も In<...> として扱う

擬似定義:

type In<T> = DeepReadonly<T> & SnapshotBrand;

2.2 component は closed-pure(自由変数禁止)

component 本体(Pure Zone)で参照可能な値は 以下のみ

  • 引数(props)
  • use.* / state/ctx 宣言で得た値(すべて In<T>
  • ローカル let/const
  • pure import された 純粋関数・静的定数
  • static const(コンパイル時定数)

禁止

  • モジュールスコープの let/var、可変シングルトン、外部キャッシュ等
  • window / document / fetch / Date.now / Math.random など ambient 値
  • fx 関数の呼び出し(Fx Zone にしか存在しない)

コンパイルエラー例:

let globalCounter = 0;

component Bad(props: In<{}>) {
  globalCounter += 1; // ❌ 外部値参照/変更(自由変数)
  return <div />;
}

2.3 import の分離

import pure { format } from "./format";  // ✅ pure 関数のみ
import fx   { createUserApi } from "./ports/userApi"; // ✅ fx でのみ使用可
  • pure import は Pure Zone で使用可能
  • fx import は Fx Zone でのみ使用可能(Pure Zone で参照不可)

3. コンポーネント宣言(入力を “見える化” する)

3.1 推奨構文:state / ctx 宣言ブロック(Design 3 強化)

component Counter(props: In<{ initial?: number }>)
  state {
    count: number = init(props.initial ?? 0);
  }
  ctx {
    locale: string <- LocaleCtx;
  }
  uses { Dom, Time }        // そのコンポーネントが “使える可能性がある” capability を宣言
{
  // Pure Zone:入力は in に集約される
  // in.props, in.state, in.ctx はすべて In<...>
  return <div>{in.state.count} ({in.ctx.locale})</div>;
}

生成される束縛(仕様)

  • in.propsprops と同値(In<Props>
  • in.state.<name> は Snapshot(In<T>
  • <name>$ は更新対象ハンドル(State<T> / Model<S,Msg> など)
    $ 付きは Snapshot ではない。render 入力ではない。

3.2 init(...) の意味

  • init(expr)初回 mount 時のみ評価される(lazy init)
  • expr は Pure でなければならない(外部アクセス不可)

4. フレームワーク(FiberFX)コア API

4.1 State:読み取りは In<T>、更新は Tx だけ

namespace use {
  pure fn state<T>(init: T | pure fn() -> T): [In<T>, State<T>];
}

type State<T> = opaque { /* 更新対象ハンドル */ };

type Tx = {
  set<T>(s: State<T>, next: T): void;
  update<T>(s: State<T>, f: pure fn(prev: In<T>) -> T): void;

  // 複数 state を手続き的にまとめたい場合
  batch(body: fx fn(tx: Tx) -> void): void;
};
  • use.state は Pure(呼ぶだけでは副作用を起こさない)
  • 更新は Fx Zone で渡される Tx 経由のみ

5. 副作用の入口(2つに限定)

5.1 イベント:event |ev, caps, tx| { ... }

JSX の onClick などに渡すハンドラは必ず event で生成する。

<button
  onClick={event |ev, { userApi }, tx| {
    tx.update(count$, c => c + 1);
    userApi.logAction({ kind: "inc" }); // 例(Port)
  }}
/>

最小権限の強制(Design 1 を採用)

  • caps の destructuring で 要求した能力しか存在しない
  • したがって { userApi } を要求しているハンドラ内で dom は使えない(型エラー)

5.2 ライフサイクル同期:use.sync / use.layoutSync

namespace use {
  // passive(paint 後)
  pure fn sync(effect: fx fn(caps: FxCaps, tx: Tx) -> void | Cleanup): void;

  // layout(commit 直後〜paint 前)
  pure fn layoutSync(effect: fx fn(caps: FxCaps, tx: Tx) -> void | Cleanup): void;

  pure fn syncOnce(effect: fx fn(caps: FxCaps, tx: Tx) -> void | Cleanup): void; // deps=[]
  pure fn syncAlways(effect: fx fn(caps: FxCaps, tx: Tx) -> void | Cleanup): void; // 毎 commit(警告推奨)
}

type Cleanup = fx fn() -> void;
  • use.sync/layoutSync 自体は Pure(登録するだけ)
  • 実行されるのは commit フェーズ以降(後述)

6. deps 自動導出(read-set / パス単位)— 本仕様の中核

6.1 “依存” の定義(仕様)

use.sync / use.layoutSync の依存集合 deps は、コールバック本体が参照する Snapshot の read-set から導出する。

  • 対象となる read は、以下の Root から始まるもの:
    • in.props / in.state / in.ctx
    • use.state 等で得た Snapshot 変数(例:count: In<number>
    • use.memo が返す In<T>(後述)

6.2 パス単位 deps(可能な限り)

read-set は可能な限り **“パス”**で表現する。

  • 例:in.state.user.id を読んだ → dep は state.user.id
  • 例:in.props.items.length を読んだ → dep は props.items.length
  • 例:in.state.map[key](動的キー) → dep は保守的に state.map

パスの形式(仕様)

  • Root{ props | state | ctx }
  • PathRoot.(identifier|[stringLiteral]|.length|...)* の列
  • 動的キー・未知の getter・外部 pure 関数に渡す場合は、最長の確定接頭辞で丸める

6.3 導出アルゴリズム(静的解析 + メタデータ)

FiberFX コンパイラは use.sync の引数(fx クロージャ)の AST を解析し、read-set を収集する。

  • 基本:静的解析で抽出
  • 追加:pure fn について、コンパイラが read-set シグネチャを生成し、呼び出し側で合成できる(設計 3 を実用化するための要点)
    • 例:pure fn getId(u: In<User>) { return u.id }
    • use.sync 内で getId(in.state.user) を呼ぶと、state.user.id を deps に入れられる

メタデータが無い pure 関数に In<T> を渡す場合は、安全側に倒して「引数のルート(または確定パス接頭辞)を読んだ」とみなす。

6.4 deps 比較(再同期判定)

  • 各 dep パスの値を commit 時点の snapshot から取り出し、Object.is で比較する
  • いずれかが変化したら cleanup → setup を行う
  • 変化していなければ何もしない

6.5 syncOnce/syncAlways(意図の明示)

  • syncOnce: deps を空集合扱い(mount/unmount のみ)
  • syncAlways: deps を特別扱いし、毎 commit 実行(基本は非推奨。計測等に限定)

7. Tx の仕様(同期スコープ限定)

7.1 Tx は “開いている間” しか使えない(仕様)

Tx は以下の Fx コールバック引数としてのみ存在する:

  • event |..., tx| { ... }
  • use.sync(|..., tx| { ... })
  • use.layoutSync(|..., tx| { ... })

禁止

  • Tx を返す / 保持する / cell に入れる / グローバルに書く
  • Tx を非同期にまたがせる(後述のルールで構文的に禁止)

7.2 非同期と Tx

本仕様では、event/sync の本体は同期関数として定義する(推奨ではなく仕様)。

  • 非同期処理は caps.sched.spawn(...) 等で開始する
  • spawn された非同期タスクから state を更新したい場合は **“送信口”**を使う

7.3 非同期更新のための “送信口”(必須機能)

Tx を async に持ち出せない以上、実用には「後で更新する」手段が必要です。
FiberFX は以下の 2 系統を用意します(どちらも Fx Zone でのみ呼べる):

A) State 用:Dispatcher

namespace use {
  pure fn dispatch<T>(s: State<T>): Dispatcher<T>;
}

type Dispatcher<T> = {
  // どちらも “新しい Tx” を内部で作って更新を enqueue する
  set(next: T): void;
  update(f: pure fn(prev: In<T>) -> T): void;
};

B) Reducer 用:Sender<Msg>(後述)

type Sender<Msg> = { send(msg: Msg): void };

これにより「Tx は同期スコープ限定」を守りつつ、通信結果などを後で state に反映できます。


8. Capability と Port(最小権限 + net 生 API 排除)

8.1 原則:アプリコードに raw net を渡さない

  • net を渡すと “なんでも叩ける” ため、原則禁止(フレームワーク側のデフォルトで提供しない)
  • 代わりに 型付き Port を提供する

8.2 Port の定義(例:UserApi)

port UserApi {
  fx fn fetchUser(id: string): Promise<User>;
  fx fn saveUser(u: User): Promise<void>;
  fx fn logAction(a: { kind: string }): Promise<void>;
}
  • port は capability の一種
  • Port のメソッドは Fx Zone でしか呼べない(Pure Zone からは呼べない)

8.3 OpenAPI 連携(方針のみ:実装はユーティリティ側)

  • OpenAPI 定義から port 相当のクライアント型を生成できる想定
  • FiberFX 側は「生成物を Port として登録できる」ことだけ保証する

8.4 Port の提供(ルートで注入)

FiberFX.createRoot(domNode, <App />, {
  ports: {
    UserApi: createUserApiFromOpenApi(/* ... */),
  }
});

8.5 使う側:uses と callback 単位の要求

component Profile(props: In<{ id: string }>)
  state { user: User | null = init(null) }
  uses { UserApi, Scheduler }
{
  let userD = use.dispatch(user$);

  use.sync(|{ UserApi, Scheduler }, _tx| {
    // Tx は同期限定なのでここでは使わず、非同期結果は Dispatcher へ
    let cancel = Scheduler.spawn(async () => {
      let u = await UserApi.fetchUser(in.props.id);
      userD.set(u);
    });

    return () => cancel.abort();
  });

  return <pre>{JSON.stringify(in.state.user)}</pre>;
}

9. Msg / reducer(大きい状態向け、オプション)

9.1 use.reducer(Elm 風を“必要な時だけ”)

namespace use {
  pure fn reducer<S, Msg>(
    init: S | pure fn() -> S,
    reduce: pure fn(state: In<S>, msg: Msg) -> S
  ): [In<S>, Model<S, Msg>];
}

type Model<S, Msg> = opaque {};
type Sender<Msg> = { send(msg: Msg): void };

type Tx = {
  // State と同様に Model にも dispatch できる
  send<S, Msg>(m: Model<S, Msg>, msg: Msg): void;
};
  • reducer 本体は 純粋
  • Msg を定義しておくと、非同期結果も Sender.send で整理できる

9.2 send(model$, msg) の sugar(Design 4 の良さを採用)

pure fn send<S, Msg>(m: Model<S, Msg>, msg: Msg): EventHandler;

使用例:

enum Msg { Inc, Dec }

component Counter(props: In<{}>)
  state {
    [count, counter$]: reducer<number, Msg>(0, (s, m) => m === Msg.Inc ? s+1 : s-1);
  }
{
  return (
    <>
      <button onClick={send(counter$, Msg.Dec)}>-</button>
      <span>{in.state.count}</span>
      <button onClick={send(counter$, Msg.Inc)}>+</button>
    </>
  );
}

小さい状態は use.state、大きい状態は use.reducer を選べる。


10. useRef 分割(要件どおり)

10.1 use.cell<T>:ミュータブル箱(render から読めない)

namespace use {
  pure fn cell<T>(init: T): Cell<T>;
}

type Cell<T> = {
  // Fx Zone でのみ可能
  get(): T;
  set(v: T): void;
};

10.2 use.nodeRef<E>:DOM ノード参照(取得/操作は Fx Zone)

namespace use {
  pure fn nodeRef<E extends HTMLElement>(): NodeRef<E>;
}

type NodeRef<E> = opaque {};

capability Dom {
  fx fn focus<E extends HTMLElement>(r: NodeRef<E>): void;
  fx fn get<E extends HTMLElement>(r: NodeRef<E>): E | null;
}

11. 実行モデル(React Fiber を踏襲しつつ仕様固定)

flowchart LR
  A[Trigger: event/sync enqueues updates] --> B[Schedule]
  B --> C[Render Phase (Pure)\ncomponent runs with In<T> snapshots]
  C --> D[Commit Phase\nDOM apply]
  D --> E[Layout Sync Phase\nuse.layoutSync]
  E --> F[Paint]
  F --> G[Passive Sync Phase\nuse.sync]
Loading

11.1 重要な保証

  • Render Phase は 中断・再実行可能(pure なので安全)
  • 副作用は Commit 以降にのみ実行(event はユーザー入力起点)
  • use.layoutSync は DOM 反映後、paint 前
  • use.sync は paint 後(必要なら非同期キューで実行)

12. 破れないこと(代表的なコンパイルエラー)

12.1 render 中に外部アクセス

component Bad(props: In<{}>) {
  Date.now(); // ❌ ambient time は存在しない(Time capability 経由のみ)
  return <div />;
}

12.2 render 中に Cell を読む

component Bad(props: In<{}>) {
  let c = use.cell(0);
  let x = c.get(); // ❌ get は Fx Zone のみ
  return <div />;
}

12.3 未要求 capability の使用

<button onClick={event |_, { UserApi }, tx| {
  Dom.focus(btnRef); // ❌ Dom を要求していない
}} />

13. 仕様として「次に詰めるべき」論点(オープン)

“完全な仕様”に近づけるには、ここを確定させる必要があります(本ドラフトでは方針のみ明記)。

  1. read-set の丸め規則の詳細

    • 動的キー、配列走査、スプレッド、unknown pure 関数呼び出し時の依存の丸め方
  2. Object.is 以外の比較戦略

    • 永続データ構造前提なら参照比較で良い
    • そうでない場合の “過剰再同期” 許容度
  3. Scheduler/Cancel の標準化

    • spawn の戻り値(AbortController 互換か、独自 token か)
    • cleanup とキャンセルの相互作用
  4. Port のスコープ(グローバル注入 / サブツリー override)

    • テストやモック差し替えをどうするか(providePort 的な仕組み)

次のステップ(確認したい)

この仕様を「実装可能なレベル」に落とすために、まず選びたいのは次のどれです:

  1. read-set の丸め規則を具体例で確定(最優先:deps の“事故”に直結)
  2. 非同期パターンの標準形(Port 呼び出し → 結果反映、キャンセル、競合解決)
  3. use.reducer の Msg 設計(ネストした Model、子コンポーネントへの Msg 伝播など)

希望の順番を指定してくれれば、その章をさらに “仕様として曖昧さが残らない” 形まで掘り下げて追記します。

@calloc134
Copy link
Author

calloc134 commented Jan 14, 2026

以下は TSX-FX / FiberFX(ドラフト v0.1) 前提の“擬似コード”サンプルです(※構文は本仕様の component / event / use.sync / capability destructuring / Tx 同期スコープ限定 / Port 利用 を意識して書いています)。


サンプル0: Port(OpenAPI 生成物を想定)と Root 注入

// ports/userApi.port.ts (生成 or 手書きの型付きPort定義)
port UserApi {
  fx fn fetchUser(id: string): Promise<User>;
  fx fn saveUser(u: User): Promise<void>;
}

type User = { id: string; name: string };
// main.tsx(アプリのブートストラップ:Pure制約の対象外でOK)
import fx { FiberFX } from "fiberfx/runtime";
import fx { createUserApiFromOpenApi } from "./generated/userApi";

FiberFX.createRoot(document.getElementById("app"), <App />, {
  ports: {
    UserApi: createUserApiFromOpenApi({ baseUrl: "/api" }),
  },
});

サンプル1: Counter(pure render + event 更新 + title 同期 + nodeRef focus)

component Counter(props: In<{ initial?: number }>)
  state {
    count: number = init(props.initial ?? 0);
  }
  uses { Dom }
{
  let btn = use.nodeRef<HTMLButtonElement>();

  // deps自動導出(パス単位想定):
  //   read: in.state.count -> deps = [state.count]
  use.sync(|{ Dom }, _tx| {
    Dom.title.set(`count=${in.state.count}`);
  });

  // layout同期(DOM反映直後〜paint前)
  use.layoutSync(|{ Dom }, _tx| {
    if (in.state.count === 0) Dom.focus(btn);
  });

  return (
    <button
      ref={btn}
      onClick={event |_, {}, tx| {
        tx.update(count$, c => c + 1);
      }}
    >
      {in.state.count}
    </button>
  );
}

サンプル2: Profile(Port + async + Txは同期限定 → Dispatcher で反映、キャンセル付き)

component Profile(props: In<{ id: string }>)
  state {
    user: User | null = init(null);
    loading: boolean = init(false);
    error: string | null = init(null);
  }
  uses { UserApi, Scheduler }
{
  // async側から更新するための “送信口”
  let userD = use.dispatch(user$);
  let loadingD = use.dispatch(loading$);
  let errorD = use.dispatch(error$);

  // deps自動導出:
  //   read: in.props.id -> deps = [props.id]
  use.sync(|{ UserApi, Scheduler }, tx| {
    // ここは同期スコープなので Tx でOK
    tx.set(loading$, true);
    tx.set(error$, null);

    // 非同期処理は spawn に逃がす(Txを await 越しに持ち出さない)
    let task = Scheduler.spawn(async () => {
      try {
        let u = await UserApi.fetchUser(in.props.id);
        userD.set(u);
      } catch (e) {
        errorD.set(String(e));
        userD.set(null);
      } finally {
        loadingD.set(false);
      }
    });

    return () => task.abort();
  });

  if (in.state.loading) return <div>Loading...</div>;
  if (in.state.error) return <div class="error">{in.state.error}</div>;
  if (!in.state.user) return <div>Not found</div>;

  return <h1>{in.state.user.name}</h1>;
}

サンプル3: Todo(大きい状態は reducer を“任意で”使用 + 永続化Port)

port TodoApi {
  fx fn load(): Promise<Todo[]>;
  fx fn save(todos: Todo[]): Promise<void>;
}

type Todo = { id: string; title: string; done: boolean };

type Msg =
  | { type: "Loaded"; todos: Todo[] }
  | { type: "Add"; title: string }
  | { type: "Toggle"; id: string }
  | { type: "Remove"; id: string };

pure fn reduce(todos: In<Todo[]>, msg: Msg): Todo[] {
  switch (msg.type) {
    case "Loaded": return msg.todos;
    case "Add":    return todos.concat([{ id: cryptoId(), title: msg.title, done: false }]);
    case "Toggle": return todos.map(t => t.id === msg.id ? { ...t, done: !t.done } : t);
    case "Remove": return todos.filter(t => t.id !== msg.id);
  }
}

// 例: pureなID生成は本当は禁じたいので、実運用なら Port/Time/Rand に寄せる。
// ここではサンプルの都合で pure fn として置いています。
pure fn cryptoId(): string { return "todo_" + "xxx"; }
component Todos(props: In<{}>)
  state {
    text: string = init("");
  }
  uses { TodoApi, Scheduler }
{
  let [todos, todos$] = use.reducer<Todo[], Msg>([], reduce);

  // asyncからMsgを投げる送信口(Txを持ち出さない)
  let todosS = use.sender(todos$);

  // mount時ロード(deps=[] を強制)
  use.syncOnce(|{ TodoApi, Scheduler }, _tx| {
    let task = Scheduler.spawn(async () => {
      let loaded = await TodoApi.load();
      todosS.send({ type: "Loaded", todos: loaded });
    });
    return () => task.abort();
  });

  // 変更のたび保存(depsは read-set から自動導出)
  //   read: todos -> deps = [todos](パス粒度なら要素参照の扱いは実装依存)
  use.sync(|{ TodoApi, Scheduler }, _tx| {
    let task = Scheduler.spawn(async () => {
      await TodoApi.save(todos);
    });
    return () => task.abort();
  });

  return (
    <div>
      <form
        onSubmit={event |ev, {}, tx| {
          ev.preventDefault();

          // reducer更新は Tx 経由で Msg を送る
          tx.send(todos$, { type: "Add", title: in.state.text });
          tx.set(text$, "");
        }}
      >
        <input
          value={in.state.text}
          onChange={event |ev, {}, tx| {
            tx.set(text$, ev.target.value);
          }}
        />
        <button type="submit">Add</button>
      </form>

      <ul>
        {todos.map(t => (
          <li key={t.id}>
            <label>
              <input
                type="checkbox"
                checked={t.done}
                onChange={send(todos$, { type: "Toggle", id: t.id })}
              />
              {t.title}
            </label>
            <button onClick={send(todos$, { type: "Remove", id: t.id })}>x</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

サンプル4: 命令的UIライブラリ統合(nodeRef + cell + layoutSync を分割してdeps制御)

// fx import(Pure Zone では使えない)
import fx { ChartLib, type ChartInstance } from "third-party-chart";
type Point = { x: number; y: number };
component ChartPanel(props: In<{ data: Point[] }>)
  uses { Dom }
{
  let host = use.nodeRef<HTMLDivElement>();
  let chart = use.cell<ChartInstance | null>(null);

  // 1) 初期化/破棄(dataを読まないので deps=[] になり、再実行されない想定)
  use.layoutSync(|{ Dom }, _tx| {
    let el = Dom.get(host);
    if (!el) return;

    if (chart.get() == null) {
      chart.set(ChartLib.create(el));
    }

    return () => {
      let c = chart.get();
      if (c != null) {
        c.destroy();
        chart.set(null);
      }
    };
  });

  // 2) データ反映(dataを読むので deps=[props.data] で更新される)
  use.layoutSync(|{}, _tx| {
    let c = chart.get();
    if (c != null) c.setData(in.props.data);
  });

  return <div ref={host} class="chart-host" />;
}

サンプル5: 外部イベント購読(syncOnce + cell で stale closure 回避)

component ResizeWatcher(props: In<{}>)
  state {
    enabled: boolean = init(true);
    width: number = init(0);
  }
  uses { Dom }
{
  let enabledCell = use.cell<boolean>(true);
  let widthD = use.dispatch(width$);

  // enabled の最新値を cell に反映(deps=[state.enabled])
  use.sync(|{}, _tx| {
    enabledCell.set(in.state.enabled);
  });

  // 購読自体は一度だけ(deps=[] を強制)
  use.syncOnce(|{ Dom }, _tx| {
    let onResize = fx |_ev| {
      if (!enabledCell.get()) return;
      widthD.set(Dom.window.innerWidth());
    };

    Dom.window.addEventListener("resize", onResize);
    // 初回も反映
    widthD.set(Dom.window.innerWidth());

    return () => Dom.window.removeEventListener("resize", onResize);
  });

  return (
    <div>
      <label>
        <input
          type="checkbox"
          checked={in.state.enabled}
          onChange={event |ev, {}, tx| tx.set(enabled$, ev.target.checked)}
        />
        enabled
      </label>

      <div>width: {in.state.width}</div>
    </div>
  );
}

必要なら次に、サンプルを追加で作れます(例えば 「フォーム送信(Port)+楽観UI+競合解決」「Context で Port を差し替えてテストする」、**「複数 effect の deps がパス単位でどう導出されるか」**など)。どの系の例が欲しいですか?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment