state_referenced_locallyの警告が出たら——propsから$stateを初期化する正しい書き方

編集フォームのように「propsで受け取った初期値をベースに、ローカルで編集可能なstateを持ちたい」という場面はよくあります。素直に書くとこうなります。

<script lang="ts">
	interface Props {
		initial: { title: string; description: string };
	}

	let { initial }: Props = $props();

	let title = $state(initial.title);
</script>

一見自然なコードですが、Svelteはこれに対してstate_referenced_locallyというコンパイラ警告を出すことがあります。

警告が指摘していること

state_referenced_locallyは、「別のリアクティブな値(stateやprops)を、別のstateの初期化式の中でそのまま読んでいる」ときに出る警告です。公式ドキュメントでは、次のような最小例で説明されています。

<script>
	let count = $state(0);
	let double = $state(count * 2); // このcountは初期値としてしか使われない
</script>

ここでのdoubleは、countが変わっても追従しません。$state(...)に渡した式はコンポーネント初期化時に一度だけ評価されるだけで、以後countを監視し続けるわけではないからです。doubleという名前を見た人は「countと連動して変わるstate」だと誤解しやすいため、Svelteはこの書き方を検知すると警告を出し、書き手に「本当に一度きりのスナップショットで良いのか」を問い直させます。

先ほどのフォームの例も構造は同じです。initialはpropsなので、親から渡されるオブジェクトが差し替われば本来リアクティブに変わりえます。しかしtitle = $state(initial.title)initial.titleを初期化時に一度読むだけで、以後initialが変わってもtitleは追従しません。

2つの直し方、それぞれの意味

この警告への対処は「連動させたいのか、させたくないのか」で分かれます。

連動させたい場合: $derivedにする

propsの変化にstateを追従させたいなら、そもそも$stateではなく$derivedが適切です。

let title = $derived(initial.title);

ただし$derivedは算出値なので、直接代入して書き換えることはできません。フォームの入力に応じてユーザーが値を編集できる必要がある場合は、この形では要件を満たせません。

一度きりのスナップショットで良い場合: untrack()で意図を明示する

編集フォームのように「初期表示のときだけpropsの値を借りて、以後はユーザーの入力で上書きしていく」という設計なら、一度きりの評価こそが正しい挙動です。この場合は`untrack()`で読み取りを明示的にリアクティブな追跡から外し、警告を解消します。

<script lang="ts">
	import { untrack } from 'svelte';

	interface Props {
		initial: { title: string; description: string };
	}

	let { initial }: Props = $props();

	let title = $state(untrack(() => initial.title));
	let description = $state(untrack(() => initial.description));
</script>

untrack(() => ...)で囲むことで、「initial.titleを読んではいるが、これは意図的な一度きりのスナップショットであり、以後の変化を追跡する気はない」という設計意図がコードから読み取れるようになります。単に警告を消すためのおまじないではなく、レビュアーや将来の自分に対する「ここはあえて非同期に切り離してある」という注釈として機能する点が、untrackを使う一番の価値です。

propsオブジェクトの差し替えに注意する

untrackで警告は消えますが、根本の挙動——「初期化時に一度だけ値を読み、以後は追従しない」——は変わりません。SvelteKitでは、同じコンポーネントインスタンスが再利用されたままdatapropだけが差し替わるケースがあるため(一覧画面から別の対象を選び直して、同じ編集フォームコンポーネントが使い回されるケースなど)、initialが変わってもフォームの表示は更新されない、という挙動が本当に意図通りかどうかは設計上の判断が必要です。意図通りでない場合は、{#key initial.id}でコンポーネントを明示的に再生成するなど、untrackとは別の手段で解決する必要があります。