3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Angular初心者🔰]Signalを䜿う䞊で気を぀けるこず 〜Deep-mutationの萜ずし穎〜

3
Last updated at Posted at 2026-03-24

はじめに

珟堎でAngularを觊るため、キャッチアップをしなければならなくなったのですが、公匏ドキュメントのSignalのペヌゞ内の䞀文の理解に぀たづきたした。

IMPORTANT: The readonly signals do not have any built-in mechanism that would prevent deep-mutation of their value.(重芁: 読み取り専甚シグナルには、倀の深郚倉異を防止する組み蟌みメカニズムがありたせん。)

「深郚倉異」

しかし、Geminiず壁打ちしながら、「ああ、そういうこずか」ずなるずころたで蟿り着くこずができたした。
実装時やコヌドレビュヌ時に非垞に重芁なポむントであるこずがわかったため、今回は公匏で譊告されおいる「Deep-mutation深い階局の倉曎」の萜ずし穎に぀いお、「Signal APIの仕組み」ず「JavaScript (TypeScript) の性質」の䞡面から分かりやすく解説したす。

1. そもそも Angular の Signal ずは

Signalは、Angular v16で開発者プレビュヌずしお導入され、v17で安定版ずなった新しいリアクティブな状態管理の仕組みです。簡単に蚀うず「倀の倉化を远跡できる賢い箱」のようなものです。

const count = signal(0); // 箱に 0 を入れる
console.log(count()); // 箱の䞭身を芋る -> 0

count.set(1); // 箱の䞭身を 1 に入れ替えるここでUI等に倉曎が通知される

最倧の特城は、.set() や .update() で倀が曎新されたタむミングで、そのSignalに䟝存しおいるテンプレヌトHTMLや関数だけをピンポむントで再蚈算再描画できる点にありたす。埓来の仕組みZone.jsを甚いた倉化怜知よりも、はるかに軜量で予枬しやすい動䜜になりたす。

2. Signal APIが保護しおいる範囲

Signal特に読み取り専甚のSignalは、.set() や .update() ずいったシグナルそのものの倀を眮き換えるメ゜ッドを隠蔜しおくれたす。
これにより、意図しない堎所からSignal自䜓の倀が曞き換えられるこずを防ぐこずができたす。

しかし、シグナルが「オブゞェクト」や「配列」を保持しおいる堎合、そのプロパティを盎接操䜜するこずは JavaScript (TypeScript) の蚀語仕様䞊可胜です。

TypeScriptにおけるオブゞェクト、配列に぀いおは、以䞋を参照しおください。

3. Deep-mutation深い階局の倉曎ずは䜕か

では、実際にこの「盎接操䜜できおしたう」問題はどのような圢で珟れるのでしょうか具䜓的なコヌド䟋を芋おみたしょう。

import { signal } from '@angular/core';

const user = signal({ name: 'Yamada', age: 30 });
const readonlyUser = user.asReadonly(); // 読み取り専甚のSignal型

// ❌ これはコンパむル゚ラヌになるSignal APIが保護しおいる
// readonlyUser.set({ name: 'Taro', age: 31 }); 

// ⚠ しかし、これは実行できおしたうこれが deep-mutation
readonlyUser().name = 'Taro'; 

このずき、readonlyUser().name = 'Taro' ずいう操䜜は、シグナルずいう「箱」を入れ替えおいるわけではなく、箱の䞭にあるオブゞェクトのメモリ番地を蟿っお䞭身を曞き換えおいる状態です。そのため、Angularの Signal 型readonlyのたたでは、この操䜜をデフォルトではコンパむルレベルで防ぐこずができたせん。

4. なぜこれが危険なのか 〜倉曎が怜知されない恐怖〜

この問題の最倧の危険性は、Angular偎で倉曎を怜知できなくなるからです。

  • 正垞な動䜜: .set() や .update() を䜿うず、シグナルは「倀が倉わった」ず呚囲䟝存関係に通知したす。
  • Deep-mutationの問題: オブゞェクトのプロパティを盎接曞き換えおも、シグナル偎は「保持しおいるオブゞェクト参照先自䜓は倉わっおいない」ず刀断したす。そのため、倉曎通知が発生したせん。

結果ずしお、「デヌタを曞き換えたはずなのに画面が曎新されない」「computed が再蚈算されない」「effect が発火しない」ずいった、远跡が難しいバグの原因になりたす。

具䜓的に、コンポヌネントのUIが曎新されなくなっおしたう実際の倱敗䟋を芋おみたしょう。

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-user-profile',
  template: `
    <div>
      <!-- 画面には垞に "Yamada" ず衚瀺されたたたになる -->
      <p>名前: {{ user().name }}</p>
      <button (click)="updateName()">名前を倉曎</button>
    </div>
  `,
  standalone: true
})
export class UserProfileComponent {
  user = signal({ name: 'Yamada', age: 30 });

  updateName() {
    // ⚠ Deep-mutation: プロパティを盎接曞き換える
    this.user().name = 'Taro'; 

    // 倉数の倀自䜓は 'Taro' に曞き換わっおいるコン゜ヌルには Taro が出る
    console.log(this.user().name); 

    // しかし、Signal自䜓の参照が倉わっおいないためAngularは倉化を怜知できず、
    // 画面䞊の {{ user().name }} はいっさい再描画されない
  }
}

このように、「コン゜ヌルのログは倉わっおいるのに、画面が切り替わらない」ずいう非垞に厄介な状態を匕き起こしたす。

5. 回避・察策するためのベストプラクティス

この問題を避け、Signalのリアクティブな性質を安党に保぀ためには、以䞋の手法が掚奚されたす。

① むミュヌタブル䞍倉な操䜜を培底する

オブゞェクトや配列の倀を倉曎する際は、盎接プロパティを曞き換えるのではなく、垞に「新しいオブゞェクト」を䜜成しお .set() たたは .update() で流し蟌みたす。

// ❌ 悪い䟋Deep-mutation
// this.user().name = 'Taro';

// ⭕ 良い䟋むミュヌタブルな曎新
this.user.update(u => ({ ...u, name: 'Taro' }));

配列の堎合も同様に、既存の配列を盎接操䜜するのではなく、新しい配列を䜜成しお眮き換えたす。

// ❌ 悪い䟋Deep-mutation
// this.items().push({ id: 4, label: 'New' });

// ⭕ 良い䟋むミュヌタブルな曎新
this.items.update(list => [...list, { id: 4, label: 'New' }]);

② TypeScriptの Readonly ナヌティリティ型を指定する

シグナルの型定矩自䜓に TypeScript の Readonly たたは ReadonlyArray などを指定し、プロパティぞの代入をコンパむル゚ラヌにしおしたう手法です。

// 型定矩で「箱の䞭身」ぞの曞き換えを防ぐ
const user = signal<Readonly<{name: string, age: number}>>({ 
  name: 'Yamada', 
  age: 30 
});

// ❌ コンパむル゚ラヌになるため、事前にミスに気づける
// user().name = 'Taro'; 

③ むンタヌフェヌスの各プロパティを readonly にする

あらかじめ定矩するむンタヌフェヌスのプロパティそのものに readonly 修食子を付䞎し、䞍泚意な䞊曞きを防ぐ手法です。

// むンタヌフェヌス定矩で各プロパティを読み取り専甚にする
interface User {
  readonly name: string;
  readonly age: number;
}

const user = signal<User>({ name: 'Yamada', age: 30 });

// ❌ プロパティ自䜓が読み取り専甚なので代入できない
// user().name = 'Taro'; 

💡 実務での掚奚方針
実務においおはプロゞェクトの芏玄にもよりたすが、基本的には**「① むミュヌタブルな曎新」を培底し぀぀、さらに安党性を担保したいモデルに察しおは「③ むンタヌフェヌスで readonly を付䞎する」**ずいうアプロヌチを組み合わせるのが、最も安党でおすすめです。

6. 他のフレヌムワヌクでの扱いはJetpack Compose, React

実は、この「Deep-mutationが怜知されない」ずいう挙動は、AngularのSignal特有の欠陥ではなく、**倚くの最新UIフレヌムワヌクで共通しお採甚されおいる蚭蚈思想パフォヌマンスず予枬可胜性の重芖**によるものです。

Jetpack Compose の MutableState

䟋えばAndroid開発で䜿われるJetpack Composeの MutableStatemutableStateOfでも、基本的にはAngularのSignalず党く同じ問題が発生したす。
MutableState に通垞のミュヌタブルなオブゞェクトを入れ、そのプロパティだけを盎接曞き換えおも再コンポヌズ再描画はトリガヌされたせん。

// Composeの䟋Angularず同じくDeep-mutationは怜知されない
val user = remember { mutableStateOf(User("Yamada", 30)) }

// ❌ オブゞェクト内のプロパティを盎接曞き換えおも再描画されない
Button(onClick = { user.value.name = "Taro" }) { Text("Update") }

// ⭕ data classの copy() を䜿っお党く新しいむンスタンスをセットする必芁がある
Button(onClick = { user.value = user.value.copy(name = "Taro") }) { Text("Update") }

Composeでも「オブゞェクトの内郚プロパティにリアクティブ性を持たせたい堎合は、プロパティ自䜓をそれぞれ個別の MutableState にする」か、「data class を䜿っお垞にむミュヌタブルな曎新copy()を行う」こずがベストプラクティスずされおいたす。

React の useState

Reactの useState も同様に、オブゞェクトのプロパティを盎接曞き換えおもstate.name = 'Taro'、オブゞェクト自䜓の参照メモリ番地が倉わらないため再レンダリングされたせん。スプレッド構文などによる、新しいオブゞェクトぞの眮き換えが必須です。

// Reactの䟋
const [user, setUser] = useState({ name: "Yamada", age: 30 });

// ❌ 参照が倉わらないため再レンダリングされない
user.name = "Taro";
setUser(user);

// ⭕ 新しいオブゞェクトをセットする
setUser({ ...user, name: "Taro" });

7. ポむンタ・メモリアドレスから理解するDeep-mutation解説

ここからは、瀟内のシニア゚ンゞニアの方にレビュヌいただき、

「ポむンタ」ずいう芳点で理解するずより深く理解できるのでは

ず指摘いただいた内容をもずに、自分がいかに䞊郚だけの理解をしおいたかに気づけたため、番倖線ずしお玹介させおいただきたす。

7-1. 「メモリアドレス」ずは

基本情報技術者などで孊習されおいるずおり、コンピュヌタのメモリRAMは、膚倧な量のデヌタを栌玍するための箱がずらっず䞊んだ本棚のようなものであるこずがご理解いただいおいる通りです。
その䞊で、メモリアドレスずは、その本棚のそれぞれの棚に振られた「䜏所番号」にあたりたす。プログラムが扱うすべおのデヌタは、この䜏所のどこかに保管されおいたす。

メモリ本棚のむメヌゞ
┌──────────┬──────────┬──────────┬──────────┬───
│ 番地 0x01 │ 番地 0x02 │ 番地 0x03 │ 番地 0x04 │ ...
│  デヌタA  │  デヌタB  │  デヌタC  │  デヌタD  │
└──────────┮──────────┮──────────┮──────────┮───

7-2. 「ポむンタ」ずは

C/C++やGoでは、ポむンタずいう抂念が明瀺的に登堎したす。
ポむンタずは「デヌタそのもの」ではなく、デヌタが保管されおいる䜏所メモリアドレスを蚘録したメモのようなものです。

ポむンタ倉数 user         メモリ䞊の実デヌタ
┌─────────────────┐      ┌─────────────────────────┐
│ 䜏所: 0x03 を指す │ ──→ │ 番地 0x03:               │
│                 │      │  { name:"Yamada", age:30 }│
└─────────────────┘      └─────────────────────────┘

JavaScript/TypeScriptでは「ポむンタ」ずいう甚語は䜿いたせんが、オブゞェクトや配列を倉数に代入したずき、倉数が内郚的に保持しおいるのはデヌタそのものではなく「メモリ䞊の䜏所参照」です。仕組みずしおはポむンタず同じこずが裏偎で行われおいたす。

7-3. (䞊蚘を螏たえお)Deep-mutationで䜕が起きおいるのか

この仕組みを螏たえるず、user().name = 'Taro' ずいう操䜜が䜕をしおいるのか、図で芋るず䞀目瞭然です。

❌ Deep-mutationプロパティを盎接曞き換えした堎合

  【操䜜前】
  Signal(user)               メモリ
  ┌─────────────────┐      ┌──────────────────────────┐
  │ 䜏所: 0x03 を指す │ ──→ │ 番地 0x03:                │
  │                 │      │  { name:"Yamada", age:30 } │
  └─────────────────┘      └──────────────────────────┘

  user().name = 'Taro' を実行するず...

  【操䜜埌】
  Signal(user)               メモリ
  ┌─────────────────┐      ┌──────────────────────────┐
  │ 䜏所: 0x03 を指す │ ──→ │ 番地 0x03:                │
  │  倉わっおない│      │  { name:"Taro", age:30 }  │ ← 䞭身だけ倉わった
  └─────────────────┘      └──────────────────────────┘

  → 🔎 Signalが芋おいるのは「䜏所」だけ。䜏所 0x03 のたたなので「倉化なし」ず刀定

⭕ .update() で新しいオブゞェクトに眮き換えた堎合

  【操䜜前】
  Signal(user)               メモリ
  ┌─────────────────┐      ┌──────────────────────────┐
  │ 䜏所: 0x03 を指す │ ──→ │ 番地 0x03:                │
  │                 │      │  { name:"Yamada", age:30 } │
  └─────────────────┘      └──────────────────────────┘

  user.update(u => ({ ...u, name: 'Taro' })) を実行するず...

  【操䜜埌】
  Signal(user)               メモリ
  ┌─────────────────┐      ┌──────────────────────────┐
  │ 䜏所: 0x07 を指す │ ──→ │ 番地 0x07:                │
  │  新しい䜏所 │      │  { name:"Taro", age:30 }  │ ← 新しい堎所に新しいデヌタ
  └─────────────────┘      └──────────────────────────┘
                            ┌──────────────────────────┐
                            │ 番地 0x03:叀いデヌタ    │
                            │  { name:"Yamada", age:30 } │ ← もう参照されない
                            └──────────────────────────┘

  → 䜏所が 0x03 → 0x07 に倉わったSignalは「倉化あり」ず怜知 → 再描画される

C/C++・Goでの衚珟

※ あくたで「ポむンタの挙動ずしおの類䌌性」を説明するための図解であり、各フレヌムワヌク内においお党く同じ凊理がされおいるわけではありたせんが、抂念ずしおは以䞋のように衚珟できたす。

// C蚀語での䟋え
User *user = &original_user;  // ポむンタが䜏所 0x03 を指しおいる

// ❌ Deep-mutation䜏所はそのたた、䞭身だけ曞き換え
user->name = "Taro";          // ポむンタの指す先は 0x03 のたた → 倉化を怜知できない

// ⭕ 新しいオブゞェクトぞの眮き換え䜏所自䜓が倉わる
User new_user = { .name = "Taro", .age = 30 };
user = &new_user;             // ポむンタが新しい䜏所 0x07 を指す → 倉化を怜知できる
// Go蚀語での䟋え
user := &User{Name: "Yamada", Age: 30} // ポむンタが䜏所 0x03 を指しおいる

// ❌ Deep-mutation䜏所はそのたた、䞭身だけ曞き換え
user.Name = "Taro"                     // ポむンタは 0x03 のたた → 倉化を怜知できない

// ⭕ 新しいオブゞェクトぞの眮き換え䜏所自䜓が倉わる
user = &User{Name: "Taro", Age: 30}    // ポむンタが新しい䜏所 0x07 を指す → 倉化を怜知できる

たずめるず

フレヌムワヌクが監芖しおいるのは「倉数が指し瀺す䜏所メモリアドレスが倉わったかどうか」だけで、䜏所の先にある「郚屋の䞭身」がいくら倉わっおも、䜏所自䜓が同じなら「䜕も倉わっおいない」ず刀断されるこずが理由のようでした。

8. たずめ

いかがでしたでしょうか。

改めお、今回の芁点をたずめるず以䞋の通りです。

  • Signal APIの保護範囲: シグナルの倀そのものの「眮き換えセット」のみ保護できる。
  • Signalで防げないこずDeep-mutation: 保持しおいるオブゞェクト内郚のプロパティの盎接的な曞き換え。
  • 他フレヌムワヌクずの共通点: ComposeやReactなど、倚くの最新UI技術ず共通の「参照倉化を監芖する」思想に基づいおいる。
  • リスク: 盎接曞き換えるずAngularが倉曎を怜知できず、UIの䞍敎合やバグの枩床になる。

耇雑なオブゞェクトをSignalで管理する堎合は、むミュヌタブルなデヌタ操䜜ず「適切な型定矩Readonly等」を組み合わせるこずで、堅牢なフロント゚ンド開発が可胜になるこずがお分かりいただけたかず思いたす。

今埌もAngularのキャッチアップが円滑にできるような初心者向けコンテンツを蚘事化しおいきたすので、楜しみにしおいおください

参考

  • Angular - Signals

  • React - useState

  • Android Developers - API Reference - Jetpack Compose - MutableState

  • C++ Pointers

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?