---
title: "リアクティビティ — zenit Zig UI ドキュメント"
description: "事実は Signal に保持し、派生値は Memo で表し、外部との同期は Effect で行います。"
url: https://zenit.z.express/ja/docs/guide/reactivity
language: ja
alternate_en: https://zenit.z.express/docs/guide/reactivity.md
alternate_zh: https://zenit.z.express/zh/docs/guide/reactivity.md
alternate_es: https://zenit.z.express/es/docs/guide/reactivity.md
alternate_ko: https://zenit.z.express/ko/docs/guide/reactivity.md
alternate_fr: https://zenit.z.express/fr/docs/guide/reactivity.md
alternate_de: https://zenit.z.express/de/docs/guide/reactivity.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# リアクティビティ

事実は Signal に保持し、派生値は Memo で表し、外部との同期は Effect で行います。

## 3 つのプリミティブ

**Signal**

書き込み可能な事実の源です。`get()` で読み、`set(v)` で書きます。読み取ると現在の計算に依存が登録されます。

**Memo**

他のリアクティブ値から遅延計算されます。入力が変わらなければ前回の結果を再利用し、値が等しければ下流に通知しません。

**Effect**

依存が変わったときに副作用を実行します。ノードの更新、ログ出力、外部システムとの同期などです。

## エンドツーエンドのデータフロー

`ui.textFmt` は渡された Signal / Memo を購読し、それらが変わるとその 1 つのテキストノードだけを再整形します。Memo の依存は明示的なコンテキスト構造体で渡します。

`counter.zig`

```zig
const count = try scope.createSignal(u32, 0);

const doubled = try scope.createMemo(u32, .{ .count = count }, struct {
    fn compute(ctx: anytype) u32 {
        return ctx.count.get() * 2;
    }
}.compute);

try root.appendChild(cx.allocator, try ui.textFmt(
    cx,
    scope,
    "count = {d}, doubled = {d}",
    .{ count, doubled },
    .{ .color = cx.tokens.color.fg_primary },
));
```

DATA FLOW

1 回の set が依存の辺を伝わり（Signal → Memo → textFmt）、render dirty になるのは購読しているテキストノードだけです。

## diff ではなく Signal

zenit はレイアウト・ヒットテスト・描画のために実際の UI ツリーを保持しますが、更新は「コンポーネントを再実行 → 候補ツリーを生成 → ツリー全体を diff」という経路を通りません。Signal を読む Memo / Effect は依存を登録し、`set` はそれらの購読者だけに通知して、影響を受けるノードを layout または render dirty にします。

SIGNAL vs DIFF

ツリーは残ります。なくなるのは、変更のたびに候補ツリーを作り直して差分を探す工程です。

[Video](https://zenit.z.express/media/reactive-counter.mp4?v=635ee40b41)

実際の Reactive Counter.app：+1 のたびに count と doubled が同時に 3 / 6 まで更新され、Reset で両方の購読結果が 0 に戻ります。

## イベントから Signal を更新

コンポーネントのコールバックは `HandlerRef` です。`cx.bindState` で Signal のポインタを持つ小さな構造体を保持し、`cx.on` でそのメソッドをコールバックに変えます。

`handlers.zig`

```zig
const Bindings = struct {
    count: *ui.Signal(u32),

    fn increment(self: *Bindings) void {
        self.count.set(self.count.get() + 1);
    }
};

const bindings = try cx.bindState(Bindings, .{ .count = count });
const on_click = cx.on(Bindings, bindings, Bindings.increment);

const plus = try ui.widgets.Button(.{
    .label = "+1",
    .on_click = on_click,
}).mount(scope, cx);
```

## 読み取りと購読

| API | 役割 |
| --- | --- |
| `signal.get()` | 読み取り、現在の Memo / Effect に依存を登録 |
| `signal.peek()` | 購読せずに読み取る。ハンドラ内でのスナップショットに便利 |
| `signal.set(v)` | 書き込み、購読者に通知 |
| `signal.update(fn)` | 前の値から次の値を計算して書き込む |
| `scope.createEffect(ctx, fn)` | 変更時に副作用を実行。Scope とともに解放 |

`effects.zig`

```zig
// Effect: sync a value to something outside the reactive graph.
try scope.createEffect(.{ .count = count }, struct {
    fn run(ctx: anytype) void {
        std.log.info("count is now {d}", .{ctx.count.get()});
    }
}.run);

// peek(): read without subscribing (no dependency is recorded).
const snapshot = count.peek();

// update(): read-modify-write in one call.
count.update(struct {
    fn inc(v: u32) u32 {
        return v + 1;
    }
}.inc);
```

## 使い分けの目安

-   **1 つの事実に 1 つの Signal。**同じ状態を通常のフィールドと Signal の両方に持たないでください。
    
-   **計算できる値は Memo に。**派生フィールドを各ハンドラで手作業で同期しないでください。
    
-   **Effect は同期だけ。**Effect 自身の依存に無条件で書き戻さないでください。ループになります。
    
-   **寿命は Scope に属します。**ページの Scope を破棄すると、その Signal・Memo・Effect も一緒に解放されます。
    

> TIP
> 
> **静的な UI にリアクティブは不要です。** 変化しないテキストや構造はそのままマウントしてください。リアクティブなプリミティブは変化するデータのためのもので、すべてのノードを包む必要はありません。
