---
title: "状態とイベント — zenit Zig UI ドキュメント"
description: "フレームをまたいで保持すべき状態は Cx に管理させ、ユーザー入力には型安全な handler で応答します。"
url: https://zenit.z.express/ja/docs/guide/state-events
language: ja
alternate_en: https://zenit.z.express/docs/guide/state-events.md
alternate_zh: https://zenit.z.express/zh/docs/guide/state-events.md
alternate_es: https://zenit.z.express/es/docs/guide/state-events.md
alternate_ko: https://zenit.z.express/ko/docs/guide/state-events.md
alternate_fr: https://zenit.z.express/fr/docs/guide/state-events.md
alternate_de: https://zenit.z.express/de/docs/guide/state-events.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# 状態とイベント

フレームをまたいで保持すべき状態は Cx に管理させ、ユーザー入力には型安全な handler で応答します。

## バインドされた状態

`cx.bindState(T, init)` は Cx の状態ストアに `T` を作成し、フレームをまたいで有効な `*T` を返します。続いて `cx.on` がそのメソッドを、どのコンポーネントでも受け取れる `HandlerRef` に変換します。状態 ID を手作業で割り当てる必要はありません。

`counter.zig`

```zig
const Counter = struct {
    allocator: std.mem.Allocator,
    value: u32 = 0,
    label: ?*ui.Node = null,
    buffer: [32]u8 = undefined,

    fn increment(self: *Counter) void {
        self.value += 1;
        const label = self.label orelse return;
        const text = std.fmt.bufPrint(
            &self.buffer,
            "Clicked {d} times",
            .{self.value},
        ) catch return;
        // Copies the text, marks it owned, frees the previous owned copy,
        // and marks the node dirty only if the text actually changed.
        label.setTextContent(self.allocator, text) catch return;
    }
};

const counter = try cx.bindState(Counter, .{ .allocator = cx.allocator });
const on_click = cx.on(Counter, counter, Counter.increment);
```

コールバック内でテキストを差し替えるには `node.setTextContent(allocator, text)` を使います。内容をコピーして owned としてマークし、以前の owned な内容を解放します。テキストが実際に変わったときだけ dirty にするので、手動の `markRenderDirty` は不要です。そのため struct は `allocator` フィールドを持っています。

> NOTE
> 
> **匿名か、アドレス指定可能か？** `bindState` は呼び出すたびに新しいエントリを作り、ID は呼び出し順に匿名で割り当てられるため、その回のマウント内でしか意味を持ちません。別の場所や次回のマウントで同じ状態を ID で取り出したい場合は、`cx.state(T, id, init)` を使い、明示的な ID を自分で割り当てます。

## コンポーネントへの接続

コンポーネントのコールバックフィールドはすべて `?HandlerRef` です。前の手順で得た `on_click` を [Button](https://zenit.z.express/ja/components/button) に渡し、更新したいテキストノードを状態 struct に記録します。

`view.zig`

```zig
const label = try ui.text(cx, "Clicked 0 times", .{
    .color = cx.tokens.color.fg_primary,
});
counter.label = label;

const button = try ui.widgets.Button(.{
    .label = "Increment",
    .variant = .primary,
    .on_click = on_click,
}).mount(scope, cx);

try root.appendChild(cx.allocator, button);
try root.appendChild(cx.allocator, label);
```

値を受け取るコールバック（[Checkbox](https://zenit.z.express/ja/components/checkbox) や Switch のチェック状態、Input や Tabs のテキストや id）には、`ui.Cx.boolHandlerFrom` のような値付きのコンストラクタを使います。

`prefs.zig`

```zig
const Prefs = struct {
    notify: bool = false,

    fn setNotify(self: *Prefs, checked: bool) void {
        self.notify = checked;
    }
};

const prefs = try cx.bindState(Prefs, .{});

const notify = try ui.widgets.Checkbox(.{
    .label_text = "Notify me",
    .on_change = ui.Cx.boolHandlerFrom(Prefs, prefs, Prefs.setNotify),
}).mount(scope, cx);
```

[Video](https://zenit.z.express/media/stories/checkbox.mp4?v=079ee6a961)

harness が Checkbox の実際のヒット領域をクリック。チェックの見た目、on\_change コールバック、状態テキストが同じフレームのパイプラインで更新されます。

## クリックの経路

プラットフォームから届くのは押下と解放であって「クリック」ではありません。ヒットテストは Z 順に手前から子を走査して最前面のノードを見つけます。押下と解放が同じ要素上で起きると、`EventDispatcher` が `click` を合成し、capture → target → bubble の 3 段階で配送してノードの `on_click` という `HandlerRef` を呼び出し、最終的にバインドしたメソッドに到達します。

EVENT ROUTING

HandlerRef は「コールバック関数 + コンテキストポインタ」にすぎません。cx.on が生成する callback はコンテキストを \*Counter に戻してメソッドを呼び出します。状態そのものは Cx の状態ストアに残り続けます。

| EventResult | 意味 |
| --- | --- |
| `.ignored` | 未処理。伝播を続け、デフォルト動作を許可 |
| `.handled` | 処理済み。伝播は続けるが、デフォルト動作を抑止 |
| `.stop` | 処理済み。伝播を停止 |

## Handler の構築方法

| コンストラクタ | メソッドシグネチャ | 用途 |
| --- | --- | --- |
| `cx.on(State, s, State.m)` | `fn (*State) void` | ボタンのクリックなど「起きた」ことの通知 |
| `ui.Cx.boolHandlerFrom(…)` | `fn (*State, bool) void` | Checkbox、Switch、Accordion |
| `ui.Cx.strHandlerFrom(…)` | `fn (*State, []const u8) void` | Input、Textarea、Tabs、Radio。スライスはコールバック中のみ有効 |
| `ui.clickable(node, handler)` | — | 任意のノードに on\_click を付ける |

## 低レベルイベント

ボタンや入力欄などのコンポーネントは、一般的なインタラクションをすでにカプセル化しています。`ui.events.Event` が必要なのは独自のインタラクションを実装するときだけです。マウス（押下、解放、click、double\_click、移動、出入り、スクロール、拡大縮小、ファイルのドラッグ）、キーボード、`text_input`、`ime_preedit` / `ime_commit`、フォーカスを扱います。キーボードコマンドには `ui.actions`、フォーカス移動には `ui.focus` を優先してください。

| 要件 | 推奨 API |
| --- | --- |
| クリック、入力、選択 | `ui.widgets.* callbacks` |
| ホバー、フォーカスリング | `ui.hooks.useHover / useFocusRing` |
| ショートカットとコマンド | `ui.actions` |
| ドラッグ、範囲ホバー | `ui.interaction.drag / range_hover` |
| 生のキー / マウス、スクロール、IME | `ui.events.Event` |

## ライフサイクルとクリーンアップ

バインドされた状態は **Cx** が所有し、Cx の破棄時に解放されます。`T` が **pub** な `deinit(*T)` を宣言していれば、フレームワークが解放直前に自動で呼び出します。ArrayList、HashMap、確保したバッファのクリーンアップはそこに書けば十分です。Signal、Memo、Effect、`onCleanup` コールバックは **Scope** に属し、`scope.dispose()` と一緒に解放されます。

OWNERSHIP

2 種類のライフサイクル：Cx レベル（ウィンドウ / アプリ全体）と Scope レベル（1 つのページやパネル）。

`editor_state.zig`

```zig
const EditorState = struct {
    allocator: std.mem.Allocator,
    lines: std.ArrayList([]u8) = .{},

    // Must be pub: the state store detects it and calls it when the Cx deinits.
    pub fn deinit(self: *EditorState) void {
        for (self.lines.items) |line| self.allocator.free(line);
        self.lines.deinit(self.allocator);
    }
};

// The initial value must not own resources yet; allocate after binding.
const editor = try cx.bindState(EditorState, .{ .allocator = cx.allocator });

// Do NOT also call scope.onCleanup(EditorState, editor, EditorState.deinit):
// the framework already calls deinit, so that would free everything twice.
```

> WARNING
> 
> **deinit を二重に呼ばないでください。** 旧版のドキュメントでは、バインドされた状態に `scope.onCleanup(T, state, T.deinit)` も登録するよう勧めていました。`deinit` が pub ならフレームワークがすでに呼び出すので、もう一度登録すると二重解放になります。

`scope.onCleanup` を使うのは 2 つの場合だけです。クリーンアップ関数が pub な `deinit` でないとき、またはリソースを Cx 全体ではなく特定の Scope（ページ）に合わせて解放する必要があるときです。

`page_cache.zig`

```zig
// A resource that must follow the page (Scope), not the whole Cx.
const PageCache = struct {
    allocator: std.mem.Allocator,
    hits: std.StringHashMapUnmanaged(u32) = .{},

    // Not named deinit, so the state store will not call it again.
    fn release(self: *PageCache) void {
        self.hits.deinit(self.allocator);
        self.hits = .{};
    }
};

const cache = try cx.bindState(PageCache, .{ .allocator = cx.allocator });
try scope.onCleanup(PageCache, cache, PageCache.release);
```

-   **初期値はリソースを持たない。**`bindState` に渡す初期値はゼロ値か、ポインタや allocator のような借用フィールドだけにします。確保が必要なものは `*T` を得てから作ります。
    
-   **allocator はフィールドとして保持する。**`deinit(*T)` の引数は 1 つだけなので、解放に使う allocator は struct 内に保存しておく必要があります。
    
-   **無効になったノードをキャッシュしない。**Node ポインタは所属するツリーと Scope が生きている間だけ有効です。ページのアンマウントや再構築の後は、状態経由で古いノードにアクセスしないでください。
