@wcstack/state 1.32.0 → 1.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ja.md CHANGED
@@ -236,6 +236,8 @@
236
236
 
237
237
  デフォルト名は `"default"`(`@` 不要)です。
238
238
 
239
+ > **非推奨 — v2 で廃止。** `name` 属性と `@name` セレクタは、パスの隣にあるもう 1 本の軸(rootNode ごとの登録簿で、Shadow 境界を越えない)です。v2 では**マウント**に置き換わります: `<wcs-state mount="cart">` が状態をルートツリーに接ぎ木し、バインディングは `cart.total` で読みます。1.x では何も変わりません。lint が使用箇所を `wcs/named-state-deprecated`(warning)で示し、ランタイムは `config.debug` 下でだけ warn します。移行の対応表: [docs/state-mount-design.md](../../docs/state-mount-design.md) §9。
240
+
239
241
  ## 状態の更新
240
242
 
241
243
  `@wcstack/state` では、すべての状態は**パス**を持ちます — `count`、`user.name`、`items` のように。状態をリアクティブに更新するには、**パスに代入**します:
@@ -1254,6 +1256,47 @@ customElements.define("my-light-component", MyLightComponent);
1254
1256
  - `data-wcs="state.message: user.name"` でホスト要素上の外部状態パスを内部コンポーネント状態プロパティにバインド
1255
1257
  - 変更はコンポーネントと外部状態間で双方向に伝播
1256
1258
 
1259
+ ### 丸ごとマウント(`state: path`)
1260
+
1261
+ プロパティ単位で配線する代わりに、ホストは自分の状態の**サブツリーを丸ごと**コンポーネントのルートとしてマウントできます。コンポーネントの中のパスは、すべてマウント先からの相対になります:
1262
+
1263
+ ```html
1264
+ <!-- ホスト側 -->
1265
+ <wcs-state json='{"user":{"name":"Alice","email":"alice@example.com"},"theme":{"mode":"light"}}'></wcs-state>
1266
+ <user-card data-wcs="state: user"></user-card>
1267
+ ```
1268
+
1269
+ ```javascript
1270
+ // コンポーネント側(Shadow DOM)
1271
+ class UserCard extends HTMLElement {
1272
+ state = {
1273
+ // マウント先の上で計算する getter — `this.name` はツリーの `user.name`
1274
+ get display() { return `${this.name} <${this.email}>`; },
1275
+ };
1276
+ constructor() {
1277
+ super();
1278
+ this.attachShadow({ mode: "open" });
1279
+ }
1280
+ connectedCallback() {
1281
+ this.shadowRoot.innerHTML = `
1282
+ <wcs-state bind-component="state"></wcs-state>
1283
+ <span data-wcs="textContent: name"></span>
1284
+ <span data-wcs="textContent: display"></span>
1285
+ <input data-wcs="value: name">
1286
+ `;
1287
+ }
1288
+ }
1289
+ customElements.define("user-card", UserCard);
1290
+ ```
1291
+
1292
+ - `state: user` はコンポーネントのルートをツリーのパス `user` に置きます。中の `name` は `user.name` **そのもの**です。読み・書き(`value: name`、`this.state.name = ...`)・getter・`for:` はすべてツリーに対して解決され、ホストの丸ごと差し替え(`this.user = {...}`)も部分書き込み(`this["user.name"] = ...`)もコンポーネントに届きます
1293
+ - 部分マウントを併用できます: `state: user; state.theme: theme` は `theme` を 2 つ目の入口としてマウントします(最長接頭辞が勝つので、中の `theme.mode` はツリーの `theme.mode` を読みます)
1294
+ - ループでは**行そのもの**をマウントします: `<template data-wcs="for: users"><user-row data-wcs="state: ."></user-row></template>`。行コンポーネントの中の `name` は `users.*.name`、中の `for: tags` は `users.*.tags.*` を回します
1295
+ - **自前のキーは私有**です([docs/state-mount-design.md](../../docs/state-mount-design.md) §4-3 の R1): コンポーネントが自分で宣言したデータキー(`state = { mode: "view" }`)はその要素のもので、ツリーには書かれません。マウント先に同名のキーがあってそれを隠す形(`user.name` の上に `state = { name: "" }`)では、ランタイムが 1 回だけ warn します(`wcs/mount-own-key-shadow`)— ツリーを読みたければ既定値を消し、私有のままにしたければ名前を変えてください
1296
+ - 配列そのものをルートにマウントする形(`state: rows` + 中で `for`)は 1.x では非対応です。行をマウントする(`state: .`)か、配列を持つオブジェクトをマウントして中で `for` を回してください(`state: group` + `for: children`)。どちらも契約テストで固定されており、マウントがツリー拡張の唯一の手段になる v2 にそのまま引き継がれます
1297
+
1298
+ > プロパティ単位の形(`state.message: user.name`)はそのまま動きます。マップされるキーに既定値を宣言しているコンポーネント(`state = { message: "" }` + `state.message: ...`)には 1.x で 1 回だけ warn が出ます: 今日はホストの値が勝ちますが、v2 では自前のキーが私有になりホストの値を隠すので、既定値を消してください。
1299
+
1257
1300
  ### 独立した Web Component への状態注入(`__e2e__/single-component`)
1258
1301
 
1259
1302
  ホストの外部状態に依存しないコンポーネントでも、`bind-component` で `state` を注入してリアクティブにできます。
@@ -1301,6 +1344,11 @@ customElements.define("my-component", MyComponent);
1301
1344
  <template data-wcs="for: users">
1302
1345
  <my-component data-wcs="state.message: .name"></my-component>
1303
1346
  </template>
1347
+
1348
+ <!-- または行そのものをマウントする: コンポーネントの中の `name` は `users.*.name` -->
1349
+ <template data-wcs="for: users">
1350
+ <user-row data-wcs="state: ."></user-row>
1351
+ </template>
1304
1352
  ```
1305
1353
 
1306
1354
  ### コンポーネント側でリストを描画する
@@ -2238,6 +2286,122 @@ bootstrapState({
2238
2286
  > が稼働中の `static wcBindable` サーフェスと sidecar manifest の drift を開発時診断として
2239
2287
  > 報告します。
2240
2288
 
2289
+ ## ページをテストする
2290
+
2291
+ `<wcs-state>` で組んだページは素の DOM なので、[happy-dom](https://github.com/capricorn86/happy-dom) でヘッドレスにテストできます — ブラウザ不要・ビルド不要・テスト専用 API 不要。レシピは 3 つ、いずれも書いてあるとおりに動きます(レシピ 1 は同じ行を実行する [`__tests__/readme.testingRecipe.test.ts`](__tests__/readme.testingRecipe.test.ts) で固定しています)。
2292
+
2293
+ 1 import で済ませたいなら [`@wcstack/testing`](../testing/README.ja.md) がレシピ 1 を `mount()` / `settle()` / `fire()` にまとめています(`<wcs-router>` も待ちます)。以下の素のレシピはそれ無しでも有効です。
2294
+
2295
+ ### 1. vitest + happy-dom
2296
+
2297
+ `vitest.config.ts`:
2298
+
2299
+ ```ts
2300
+ import { defineConfig } from "vitest/config";
2301
+
2302
+ export default defineConfig({
2303
+ test: { environment: "happy-dom", setupFiles: ["./tests/setup.ts"] },
2304
+ });
2305
+ ```
2306
+
2307
+ `tests/setup.ts` — 要素の登録を 1 回だけ行い、インラインの `<script type="module">` state を `data:` URL ローダーに回します(Node は `blob:` URL を import できないため、この行が無いとインライン script の state は永久に読み込み中になります):
2308
+
2309
+ ```ts
2310
+ import { bootstrapState } from "@wcstack/state";
2311
+
2312
+ bootstrapState();
2313
+ URL.createObjectURL = undefined as any;
2314
+ ```
2315
+
2316
+ テスト:
2317
+
2318
+ ```ts
2319
+ import { expect, it } from "vitest";
2320
+ import { getBindingsReady } from "@wcstack/state";
2321
+
2322
+ const settle = () => new Promise<void>((r) => setTimeout(r, 0));
2323
+
2324
+ it("描画・再描画・ハンドラ実行", async () => {
2325
+ // 1. テスト対象の断片をマウント
2326
+ document.body.innerHTML = `
2327
+ <wcs-state json='{"count": 1, "items": ["apple", "banana"]}'></wcs-state>
2328
+ <p id="count" data-wcs="textContent: count"></p>
2329
+ <ul id="items">
2330
+ <template data-wcs="for: items">
2331
+ <li data-wcs="textContent: items.*"></li>
2332
+ </template>
2333
+ </ul>
2334
+ `;
2335
+
2336
+ // 2. state 要素を待ち、続けて `document` 配下の全バインドを待つ
2337
+ const stateEl = document.querySelector("wcs-state") as any;
2338
+ await stateEl.connectedCallbackPromise;
2339
+ await getBindingsReady(document);
2340
+
2341
+ // 3. 初期描画を検証
2342
+ expect(document.querySelector("#count")!.textContent).toBe("1");
2343
+ expect(document.querySelectorAll("#items li").length).toBe(2);
2344
+
2345
+ // 4. writable プロキシ経由で書く — ハンドラがやっていることと同じ
2346
+ await stateEl.createStateAsync("writable", async (state: any) => {
2347
+ state.count = 42;
2348
+ state.items = [...state.items, "cherry"];
2349
+ });
2350
+ await settle();
2351
+
2352
+ // 5. 再描画を検証
2353
+ expect(document.querySelector("#count")!.textContent).toBe("42");
2354
+ expect(document.querySelectorAll("#items li").length).toBe(3);
2355
+ });
2356
+ ```
2357
+
2358
+ ユーザー操作と同じ経路で動かすなら、state はインライン(メソッド込み)のまま DOM イベントを発火します。`data-wcs="onclick: up"` のハンドラは `button.click()` で走り、`settle()` 1 回の後に DOM へ反映されます。
2359
+
2360
+ - `getBindingsReady(root)` は `root`(`document` か shadow root)配下の全バインド構築が終わると resolve し、バインド初期化に失敗すると reject します(v1.26+)。
2361
+ - 更新はマイクロタスク境界で収束します。書き込み後の `setTimeout(0)` 1 回で十分です。
2362
+ - `state.items = [...state.items, "cherry"]` がリアクティブな書き方です — `state.items.push()` は観測されません(ハンドラ内と同じ規則)。
2363
+ - happy-dom は `customElements.define` 時に既存ノードを**差し替えて**アップグレードします。「遅れて define された同一ノードに値が届く」はヘッドレスでは検証できません。happy-dom と実ブラウザのイベントタイミング差ももう 1 つの死角なので、そこは実ブラウザ e2e(Playwright)を 1 本残してください。
2364
+ - happy-dom の `textContent` setter は数値 `0` を空文字にします(ブラウザは `"0"`)。このレシピでは `textContent: count` のバインドが 0 のとき `""` に読めます。state の値で assert するか、setter をシムする `@wcstack/testing` の `mount()` を使ってください。
2365
+
2366
+ ### 2. 素の Node(vitest なし)
2367
+
2368
+ `@wcstack/server` が SSR に使っているグローバル差し替えをそのまま export しているので再利用します。**`@wcstack/state` は `installGlobals` の後に動的 import** してください — 要素クラスはモジュール評価時に基底クラスを決めるので、ファイル先頭で静的 import すると happy-dom が構築できない要素が登録されます:
2369
+
2370
+ ```js
2371
+ import { Window } from "happy-dom";
2372
+ import { installGlobals } from "@wcstack/server";
2373
+
2374
+ const window = new Window({ url: "http://localhost/" });
2375
+ const restore = installGlobals(window); // document, customElements, HTMLElement, ...(GLOBALS_KEYS)
2376
+ try {
2377
+ const { bootstrapState, getBindingsReady } = await import("@wcstack/state");
2378
+ bootstrapState();
2379
+ // ... 以降はレシピ 1 と同じ mount / await / assert
2380
+ } finally {
2381
+ restore();
2382
+ await window.happyDOM.close();
2383
+ }
2384
+ ```
2385
+
2386
+ `installGlobals` は `URL.createObjectURL` の無効化も行うので、インライン script の state はレシピ 1 と同じ経路で読み込まれます。
2387
+
2388
+ ### 3. 描画結果のスナップショット
2389
+
2390
+ `@wcstack/server` の [`renderToString()`](../server/README.ja.md) は描画済みマークアップを文字列で返します。保存したスナップショットと比較してください:
2391
+
2392
+ ```ts
2393
+ import { expect, it } from "vitest";
2394
+ import { renderToString } from "@wcstack/server";
2395
+
2396
+ it("描画結果がスナップショットと一致する", async () => {
2397
+ const html = await renderToString(`
2398
+ <wcs-state json='{"items": ["apple", "banana"]}' enable-ssr></wcs-state>
2399
+ <ul><template data-wcs="for: items"><li data-wcs="textContent: items.*"></li></template></ul>
2400
+ `);
2401
+ expect(html).toMatchSnapshot();
2402
+ });
2403
+ ```
2404
+
2241
2405
  ## TypeScript サポート
2242
2406
 
2243
2407
  `defineState()` で状態オブジェクトをラップすると、メソッドや getter 内の `this` に型補完が効きます。ランタイムコストはゼロ(アイデンティティ関数)です。
package/README.md CHANGED
@@ -236,6 +236,8 @@ Multiple state elements can coexist with the `name` attribute. Bindings referenc
236
236
 
237
237
  Default name is `"default"` (no `@` needed).
238
238
 
239
+ > **Deprecated — removed in v2.** The `name` attribute and the `@name` selector are a second axis next to the path (a per-rootNode registry that does not cross shadow boundaries). v2 replaces them with **mounts**: `<wcs-state mount="cart">` grafts the state onto the root tree, and bindings read it as `cart.total`. Nothing changes in 1.x; the linter reports the sites as `wcs/named-state-deprecated` (warning) and the runtime warns only under `config.debug`. Migration table: [docs/state-mount-design.md](../../docs/state-mount-design.md) §9.
240
+
239
241
  ## Updating State
240
242
 
241
243
  In `@wcstack/state`, every piece of state has a **path** — like `count`, `user.name`, or `items`. To update state reactively, **assign to the path**:
@@ -1255,6 +1257,47 @@ customElements.define("my-light-component", MyLightComponent);
1255
1257
  - `data-wcs="state.message: user.name"` on the host element binds outer state paths to inner component state properties
1256
1258
  - Changes propagate bidirectionally between the component and the outer state
1257
1259
 
1260
+ ### Whole-object Mount (`state: path`)
1261
+
1262
+ Instead of wiring the component's state property by property, the host can mount a **whole subtree** of its state as the component's root. Inside the component every path is then relative to the mount point:
1263
+
1264
+ ```html
1265
+ <!-- Host -->
1266
+ <wcs-state json='{"user":{"name":"Alice","email":"alice@example.com"},"theme":{"mode":"light"}}'></wcs-state>
1267
+ <user-card data-wcs="state: user"></user-card>
1268
+ ```
1269
+
1270
+ ```javascript
1271
+ // Component (Shadow DOM)
1272
+ class UserCard extends HTMLElement {
1273
+ state = {
1274
+ // a getter computed over the mount — `this.name` is the tree's `user.name`
1275
+ get display() { return `${this.name} <${this.email}>`; },
1276
+ };
1277
+ constructor() {
1278
+ super();
1279
+ this.attachShadow({ mode: "open" });
1280
+ }
1281
+ connectedCallback() {
1282
+ this.shadowRoot.innerHTML = `
1283
+ <wcs-state bind-component="state"></wcs-state>
1284
+ <span data-wcs="textContent: name"></span>
1285
+ <span data-wcs="textContent: display"></span>
1286
+ <input data-wcs="value: name">
1287
+ `;
1288
+ }
1289
+ }
1290
+ customElements.define("user-card", UserCard);
1291
+ ```
1292
+
1293
+ - `state: user` mounts the component's root at the tree path `user`: `name` inside the component **is** `user.name`. Reads, writes (`value: name`, `this.state.name = ...`), getters and `for:` all resolve against the tree; the host's `this.user = {...}` replacement and `this["user.name"] = ...` writes both reach the component.
1294
+ - A partial mount can sit next to it: `state: user; state.theme: theme` mounts `theme` as a second entry point (longest prefix wins, so `theme.mode` inside the component reads the tree's `theme.mode`).
1295
+ - In a loop, mount **the row itself**: `<template data-wcs="for: users"><user-row data-wcs="state: ."></user-row></template>`. Inside the row component `name` is `users.*.name`, and its own `for: tags` runs over `users.*.tags.*`.
1296
+ - **Own keys are private** (rule R1 in [docs/state-mount-design.md](../../docs/state-mount-design.md) §4-3): a data key the component declares itself (`state = { mode: "view" }`) belongs to that element and is never written to the tree. If it hides a key that exists at the mount point (`state = { name: "" }` mounted over `user.name`), the runtime warns once (`wcs/mount-own-key-shadow`) — remove the default to read the tree, or rename it to keep it private.
1297
+ - Mounting an array as the root (`state: rows` with `for` over it inside) is not supported in 1.x; mount the row (`state: .`) or the object that holds the array (`state: group` with `for: children` inside). Both forms are contract-tested and carry over unchanged to v2, where mounts become the only way to extend the tree.
1298
+
1299
+ > The per-property form (`state.message: user.name`) keeps working. A component that declares a default for a mapped key (`state = { message: "" }` together with `state.message: ...`) gets a one-time warning in 1.x: today the host value wins, in v2 the own key becomes private and would hide it — drop the default.
1300
+
1258
1301
  ### Standalone Web Component Injection (`__e2e__/single-component`)
1259
1302
 
1260
1303
  Even when a component is independent from outer host state, you can inject reactive state with `bind-component`.
@@ -1302,6 +1345,11 @@ customElements.define("my-component", MyComponent);
1302
1345
  <template data-wcs="for: users">
1303
1346
  <my-component data-wcs="state.message: .name"></my-component>
1304
1347
  </template>
1348
+
1349
+ <!-- or mount the row itself: inside the component, `name` is `users.*.name` -->
1350
+ <template data-wcs="for: users">
1351
+ <user-row data-wcs="state: ."></user-row>
1352
+ </template>
1305
1353
  ```
1306
1354
 
1307
1355
  ### Rendering a List Inside the Component
@@ -2246,6 +2294,122 @@ the short answer is that translations belong on a path, not in a filter.
2246
2294
  > `analyzeContract()` API reports drift between a live `static wcBindable` surface and
2247
2295
  > a sidecar manifest for dev-time diagnostics.
2248
2296
 
2297
+ ## Testing Your Page
2298
+
2299
+ A page built on `<wcs-state>` is plain DOM, so it can be tested headlessly with [happy-dom](https://github.com/capricorn86/happy-dom) — no browser, no build step, no test-only API. Three recipes follow; every one of them runs as written (recipe 1 is pinned by [`__tests__/readme.testingRecipe.test.ts`](__tests__/readme.testingRecipe.test.ts), which executes the same lines).
2300
+
2301
+ Want it as one import? [`@wcstack/testing`](../testing/README.md) packages recipe 1 as `mount()` / `settle()` / `fire()` (and waits for `<wcs-router>` too). The bare recipes below stay valid without it.
2302
+
2303
+ ### 1. vitest + happy-dom
2304
+
2305
+ `vitest.config.ts`:
2306
+
2307
+ ```ts
2308
+ import { defineConfig } from "vitest/config";
2309
+
2310
+ export default defineConfig({
2311
+ test: { environment: "happy-dom", setupFiles: ["./tests/setup.ts"] },
2312
+ });
2313
+ ```
2314
+
2315
+ `tests/setup.ts` — register the elements once, and route inline `<script type="module">` state through the `data:` URL loader (Node cannot import `blob:` URLs; without this line an inline-script state never finishes loading):
2316
+
2317
+ ```ts
2318
+ import { bootstrapState } from "@wcstack/state";
2319
+
2320
+ bootstrapState();
2321
+ URL.createObjectURL = undefined as any;
2322
+ ```
2323
+
2324
+ A test:
2325
+
2326
+ ```ts
2327
+ import { expect, it } from "vitest";
2328
+ import { getBindingsReady } from "@wcstack/state";
2329
+
2330
+ const settle = () => new Promise<void>((r) => setTimeout(r, 0));
2331
+
2332
+ it("renders, re-renders, and runs handlers", async () => {
2333
+ // 1. Mount the fragment under test
2334
+ document.body.innerHTML = `
2335
+ <wcs-state json='{"count": 1, "items": ["apple", "banana"]}'></wcs-state>
2336
+ <p id="count" data-wcs="textContent: count"></p>
2337
+ <ul id="items">
2338
+ <template data-wcs="for: items">
2339
+ <li data-wcs="textContent: items.*"></li>
2340
+ </template>
2341
+ </ul>
2342
+ `;
2343
+
2344
+ // 2. Wait for the state element, then for every binding under `document`
2345
+ const stateEl = document.querySelector("wcs-state") as any;
2346
+ await stateEl.connectedCallbackPromise;
2347
+ await getBindingsReady(document);
2348
+
2349
+ // 3. Assert the initial render
2350
+ expect(document.querySelector("#count")!.textContent).toBe("1");
2351
+ expect(document.querySelectorAll("#items li").length).toBe(2);
2352
+
2353
+ // 4. Write through a writable proxy — exactly what a handler does
2354
+ await stateEl.createStateAsync("writable", async (state: any) => {
2355
+ state.count = 42;
2356
+ state.items = [...state.items, "cherry"];
2357
+ });
2358
+ await settle();
2359
+
2360
+ // 5. Assert the re-render
2361
+ expect(document.querySelector("#count")!.textContent).toBe("42");
2362
+ expect(document.querySelectorAll("#items li").length).toBe(3);
2363
+ });
2364
+ ```
2365
+
2366
+ To drive the page the way a user does, keep the state inline (methods included) and dispatch DOM events; a `data-wcs="onclick: up"` handler runs on `button.click()`, and the DOM reflects the write after one `settle()`.
2367
+
2368
+ - `getBindingsReady(root)` resolves once every binding under `root` (a `document` or a shadow root) is built, and rejects if binding initialization fails (v1.26+).
2369
+ - Updates settle on the microtask queue; a single `setTimeout(0)` after a write is enough.
2370
+ - `state.items = [...state.items, "cherry"]` is the reactive form — `state.items.push()` is not observed (same rule as in handlers).
2371
+ - Under happy-dom, `customElements.define` upgrades existing nodes by **replacing** them; "a value reaches the same node after a late define" cannot be asserted headlessly. Event timing differences between happy-dom and real browsers are the other blind spot — keep one browser e2e (Playwright) for those.
2372
+ - happy-dom's `textContent` setter turns a numeric `0` into an empty string (browsers render `"0"`), so a `textContent: count` binding reads `""` at zero in this recipe. Assert on the state value, or use `@wcstack/testing`, whose `mount()` shims the setter.
2373
+
2374
+ ### 2. Bare Node (no vitest)
2375
+
2376
+ `@wcstack/server` already exports the globals swap it uses for SSR; reuse it. **Import `@wcstack/state` dynamically after `installGlobals`** — the element classes pick their base class when the module is evaluated, so a static import at the top of the file registers elements that happy-dom cannot construct:
2377
+
2378
+ ```js
2379
+ import { Window } from "happy-dom";
2380
+ import { installGlobals } from "@wcstack/server";
2381
+
2382
+ const window = new Window({ url: "http://localhost/" });
2383
+ const restore = installGlobals(window); // document, customElements, HTMLElement, ... (GLOBALS_KEYS)
2384
+ try {
2385
+ const { bootstrapState, getBindingsReady } = await import("@wcstack/state");
2386
+ bootstrapState();
2387
+ // ... the same mount / await / assert steps as recipe 1
2388
+ } finally {
2389
+ restore();
2390
+ await window.happyDOM.close();
2391
+ }
2392
+ ```
2393
+
2394
+ `installGlobals` also disables `URL.createObjectURL` for you, so inline-script state loads the same way as in recipe 1.
2395
+
2396
+ ### 3. Snapshot the rendered HTML
2397
+
2398
+ [`renderToString()`](../server/README.md) from `@wcstack/server` returns the fully rendered markup as a string; compare it against a stored snapshot:
2399
+
2400
+ ```ts
2401
+ import { expect, it } from "vitest";
2402
+ import { renderToString } from "@wcstack/server";
2403
+
2404
+ it("matches the rendered snapshot", async () => {
2405
+ const html = await renderToString(`
2406
+ <wcs-state json='{"items": ["apple", "banana"]}' enable-ssr></wcs-state>
2407
+ <ul><template data-wcs="for: items"><li data-wcs="textContent: items.*"></li></template></ul>
2408
+ `);
2409
+ expect(html).toMatchSnapshot();
2410
+ });
2411
+ ```
2412
+
2249
2413
  ## TypeScript Support
2250
2414
 
2251
2415
  `defineState()` wraps your state object and provides type-safe `this` inside methods and getters — with zero runtime cost (identity function).