untrackedとcomputedシグナルの違い|Angular・SolidJS・Preactで依存追跡を外す使い方
untracked(SolidJSではuntrack)は、リアクティブなシグナルの値を読むだけで依存関係を作らないための仕組みです。通常のシグナル読み取りは、それを囲むcomputedやeffectの依存として自動登録され、値が変わるたびに再計算・再実行が走ります。untrackedは、その追跡だけを一時的に外して現在値を取り出します。この記事では、Angular・SolidJS・Preact・標準化提案(TC39)それぞれの実APIと、対策の要である「計算されたシグナル(computed signals)との違い」、effect内での使いどころと落とし穴までを実コードで整理します。
まとめ:untrackedの役割とcomputedとの違い
- untrackedの役割:シグナルを読むが依存にしない。
effectやcomputedの再実行トリガーから特定の読み取りだけを除外する。 - API名はフレームワークで異なる:Angular
untracked()/SolidJSuntrack()/Preactuntracked()・signal.peek()/TC39標準案Signal.subtle.untrack()。引数はいずれも「読み取りを行う関数」。 - computedとの違い:
computedは依存を追跡して自動更新される派生値。untrackedは追跡しない読み取り。computedの中でuntrackedを使うと、その値が変わっても再計算されず「古い値のまま」になる(=意図した時だけ使う)。 - 主用途は2つ:付随的な読み取り(ログ・設定値)を依存から外す/
effect内で別シグナルを更新する際の無限ループ回避。 - 使いすぎない:依存を隠すため、UIが更新されないバグの温床になる。単一値の非追跡読み取りはPreactなら
peek()で足りる。
untrackedとは — シグナルの依存追跡を外す仕組み
シグナルベースのリアクティビティでは、effectやcomputedの中でシグナルのゲッターを呼ぶと、そのシグナルが「依存」として記録されます。以後そのシグナルが変わると、記録したeffect/computedが再実行されます。これが自動更新の正体です。
untrackedは、この記録処理だけを一時停止して値を読みます。読み取った値は使えますが、そのシグナルの変更では再実行されません。「今の値は欲しいが、この値の変化に反応はしたくない」場面のためのAPIで、Angular公式は「参照のために値は必要だが、その変化でeffectを走らせたくないときに使う」と説明しています。
フレームワーク別のuntracked API と書き方
概念は共通ですが、関数名・パッケージ・引数の渡し方が異なります。いずれも引数は「シグナルを読む関数」を1つ取り、その戻り値をそのまま返します。
Angular Signals:untracked()
Angular(v16以降のSignals)は@angular/coreのuntracked()を提供します。effectやcomputedの内側で、依存にしたくない読み取りをuntracked(() => sig())で囲みます。
import { signal, computed, effect, untracked } from '@angular/core';
const count = signal(0);
const unit = signal('件');
// count の変化でだけ再実行される。unit は追跡しない
effect(() => {
const n = count(); // 依存として登録される
const u = untracked(() => unit()); // 付随的な読み取り=依存にしない
console.log(`${n}${u}`);
});
上のeffectはcountの変更でのみ再実行され、unitを変えても走りません。
SolidJS:untrack()
SolidJSはsolid-jsのuntrack()です。関数名が単数形のuntrackである点に注意してください。createEffect内で、追跡したくない読み取りを囲みます。
import { createSignal, createEffect, untrack } from 'solid-js';
const [count, setCount] = createSignal(0);
const [unit, setUnit] = createSignal('件');
createEffect(() => {
// count() は追跡、untrack 内の unit() は非追跡
console.log(count(), untrack(() => unit()));
});
Preact Signals:untracked() と peek()
Preactの@preact/signals-coreはuntracked()を持ちます。公式はuntracked()をsignal.peek()の代替として位置づけており、複数のシグナルをまとめて非追跡で読めるぶんだけuntrackedが有利です。単一シグナルの値を追跡なしで取るだけならpeek()が簡潔です。
import { signal, untracked } from '@preact/signals-core';
const count = signal(0);
const unit = signal('件');
// Preact のシグナルは .value で読む。untracked は複数シグナルをまとめて非追跡で読める
const snapshot = untracked(() => count.value + unit.value);
// 単一シグナルの値だけ欲しいなら peek() が簡潔(同じく非追跡)
const now = count.peek();
TC39標準案:Signal.subtle.untrack()
JavaScriptにシグナルを追加するTC39提案(Angular・Solid・Preact・Vue等の開発者が設計に参加)では、untrackはSignal.subtle名前空間に置かれています。subtleという名は「避難ハッチであり、扱いを誤ると壊れる上級者向けAPI」であることを示す意図的な命名です。標準はまだ提案段階で、実務では各フレームワークのAPIを使います。
// TC39 Signals 提案(標準化検討中)
const count = new Signal.State(0);
const unit = new Signal.State('件');
const label = new Signal.Computed(() =>
// subtle 名前空間=上級者向けの避難ハッチ。State は .get() で読む
Signal.subtle.untrack(() => unit.get()) + count.get()
);
untracked と計算されたシグナル(computed signals)の違い
両者は名前が似ていますが役割は逆です。computed(計算されたシグナル)は依存を追跡して自動更新される派生値、untrackedは追跡しない読み取りです。混同しやすいのは、computedの中でuntrackedを使える点です。
| 観点 | computed(計算されたシグナル) | untracked |
|---|---|---|
| 目的 | 複数シグナルから派生値を作る | 値を読むが依存にしない |
| 依存追跡 | する(自動更新) | しない |
| 再計算 | 依存が変わると走る | 再計算のトリガーにならない |
| 使う場所 | 単体、または他のcomputed/effect内 | computed/effectの内側 |
下はcomputedの中でuntrackedを使った例です(以降のコードはAngular Signalsの関数呼び出し記法。SolidJSもほぼ同形、Preactは.value・TC39は.get()に読み替えます)。lastをuntrackedで読んでいるため、lastを更新してもfullNameは再計算されません。これは「初回の値だけ固定したい」意図があるときは正しく、そうでなければ更新漏れのバグになります。
const first = signal('太郎');
const last = signal('田中');
// last を untracked で読むと、last を変えても fullName は再計算されない
const fullName = computed(() => `${untracked(() => last())} ${first()}`);
// → computed 内の untracked は「古い値のまま」を招きやすい
effect内でuntrackedを使う典型パターン
付随的な読み取りを依存から外す
effectが本来反応すべきシグナルは限られているのに、途中でログ用の設定値やコンテキスト値を読むと、それらまで依存に入って余計な再実行を招きます。ログ・設定・現在時刻のような「参照するだけ」の値をuntrackedで囲むと、effectは本命のシグナルだけに反応します。前掲のAngular例(unitをuntracked)がこのパターンです。
他シグナルを更新する際の無限ループ回避
effectの中であるシグナルを読み、かつ別(または同じ)シグナルをsetすると、そのsetが自分自身を再発火させて無限ループになることがあります。更新対象をuntrackedで読めば、その読み取りは依存にならずループを断てます。
const source = signal(0);
const logs = signal<string[]>([]);
effect(() => {
const v = source(); // これだけ追跡する
// logs を追跡すると set() で自分を再発火=無限ループ。untracked で回避
untracked(() => logs.set([...logs(), `value=${v}`]));
});
このeffectはsourceの変更でだけ動き、logsを更新しても再発火しません。
なお、Angular 22.1ではHttpClientのインターセプタがeffect内で自動的に非追跡の扱いになり、インターセプタからシグナルを読んでも依存に入らなくなりました。認証トークンをシグナルで保持している実装で効いてくる変更です。詳細はAngular 22.1とは?v22系の変更点とリリース体制の転換を実装目線で解説で扱っています。
untrackedを使うべきでない場面と落とし穴
untrackedは依存関係を意図的に隠すAPIなので、乱用すると「シグナルを変えたのにUIが更新されない」タイプのバグを生みます。TC39提案がuntrackをsubtle(上級者向け)に置いているのも、この危険性を明示するためです。次の指針で使いどころを絞ってください。
- computed内での常用は避ける:派生値が依存の変化に追従しなくなる。派生は原則すべて追跡し、固定したい理由が明確なときだけ
untrackedを使う。 - 単一値の非追跡読み取りはpeekで足りる:Preactなら
signal.peek()。関数でラップするuntrackedより意図が明確。 - 「なぜ追跡を外すか」をコメントで残す:依存が見えなくなるぶん、後から読む人に意図が伝わらないと保守性が落ちる。
- パフォーマンス目的の先回りはしない:シグナルの再計算は差分のみで軽い。実測で過剰な再実行を確認してから外す。
よくある質問
untrackedとuntrackはどちらが正しい名前ですか?
どちらも正しく、フレームワークで名前が違うだけです。Angularとpreact/signalsはuntracked()、SolidJSはuntrack()です。機能は同じで「シグナルを追跡せずに読む」ものです。
untrackedとsignal.peek()の違いは?
目的は同じ(追跡なしで読む)です。peek()は1つのシグナルの値を取る簡潔な書き方、untracked(fn)は関数を渡して複数の読み取りをまとめて非追跡にできる書き方です。単一値ならpeek()、複数や式の評価をまとめたいならuntrackedが向きます。
computedの中でuntrackedを使っても良いですか?
使えますが慎重に。computed内でuntracked読みした値は、その値が変わっても再計算のトリガーになりません。「初回だけ固定したい」など意図が明確な場合に限り、コメントを添えて使ってください。既定はすべて追跡する(=untrackedを使わない)方が安全です。
Reactにuntrackedはありますか?
ReactはSignalsではなく再レンダリングモデルのため、同名APIはありません。同種の「値は読むが再レンダリングのトリガーにしない」用途にはuseRefが近い役割を果たします。SolidJSやPreact SignalsはこのSignalモデルを採用しているためuntrack/untrackedを持ちます。
関連記事
- SolidJSとは何か?基本的な特徴とReactとの違い —
untrackを持つSignal/Memoベースのリアクティビティを採用したフレームワーク。 - Angular v21 の概要と主な変更点 — Signalベースの描画とzoneless化。
untracked()が属するAngular Signalsの最新動向。 - Leptosとは?Rust製フルスタックWebフレームワーク — Signal・Memo・Effectで構成するリアクティブモデルの別実装例。