$effect
effect とは、アプリケーションに「動作」をさせるものを指します。Svelte が effect 関数を実行すると、その関数内でアクセスされる state (および derived state) を追跡し (ただし、untrack
内でアクセスされた場合は除く)、その state が後で変更されると関数を再実行します。
Svelte アプリのほとんどの effect は Svelte 自体によって作成されます — 例えば、name
が変更されたときに <h1>hello {name}!</h1>
のテキストを更新する部分です。
しかし、自分自身で $effect
rune を使用して独自の effect を作成することもできます。これは、外部システム (ライブラリや <canvas>
要素、またはネットワークをまたぐ何か) を Svelte アプリ内の state と同期する必要がある場合に便利です。
$effect
の使い過ぎには注意してください! effect 内で過剰な作業を行うと、コードが理解しにくく、保守が困難になることがあります。代替アプローチについては$effect
を使うべきでないとき を参照してください。
effect は、コンポーネントが DOM にマウントされた後に実行され、state の変更後には マイクロタスク 内で実行されます (デモ):
<script>
let size = $state(50);
let color = $state('#ff3e00');
let canvas;
$effect(() => {
const context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
// これは `color` や `size` が変更されるたびに再実行されます
context.fillStyle = color;
context.fillRect(0, 0, size, size);
});
</script>
<canvas bind:this={canvas} width="100" height="100" />
再実行はバッチで処理され(例えば、color
と size
を同時に変更しても、別々の再実行は発生しません)、DOM 更新が適用された後に発生します。
$effect
は、コンポーネント初期化中 (または親の effect がアクティブな間) に呼び出される限り、コンポーネントのトップレベルだけでなくどこにでも配置できます。それはコンポーネント (または親の effect) のライフサイクルに結び付けられるため、コンポーネントがアンマウントされるとき (または親の effect が破棄されるとき) に自動的に破棄されます。
$effect
から関数を返すことができ、その関数は effect が再実行される直前および破棄される直前に実行されます (デモ)。
<script>
let count = $state(0);
let milliseconds = $state(1000);
$effect(() => {
// これは `milliseconds` が変更されるたびに再作成されます
const interval = setInterval(() => {
count += 1;
}, milliseconds);
return () => {
// コールバックが提供される場合、以下のときに実行されます
// a) effect が再実行される直前
// b) コンポーネントが破棄される前
clearInterval(interval);
};
});
</script>
<h1>{count}</h1>
<button onclick={() => (milliseconds *= 2)}>slower</button>
<button onclick={() => (milliseconds /= 2)}>faster</button>
依存関係を理解する
$effect
は、その関数内で 同期的に 読み取られたリアクティブな値 ($state
, $derived
, $props
) を自動的に (関数呼び出しによる関節的なものも含めて) 検出し、それらを依存関係として登録します。それらの依存関係が変更されたとき、$effect
は再実行をスケジュールします。
非同期的に読み取られる値 (例えば、await
の後や setTimeout
内) は追跡されません。この例では、color
が変更されると canvas が再描画されますが、size
が変更されても再描画されません (デモ)。
function $effect(fn: () => void | (() => void)): void
namespace $effect
Runs code when a component is mounted to the DOM, and then whenever its dependencies change, i.e. $state
or $derived
values.
The timing of the execution is after the DOM has been updated.
Example:
$effect(() => console.log('The count is now ' + count));
If you return a function from the effect, it will be called right before the effect is run again, or when the component is unmounted.
Does not run during server side rendering.
$effect(() => {
const const context: CanvasRenderingContext2D
context = let canvas: {
width: number;
height: number;
getContext(type: "2d", options?: CanvasRenderingContext2DSettings): CanvasRenderingContext2D;
}
canvas.function getContext(type: "2d", options?: CanvasRenderingContext2DSettings): CanvasRenderingContext2D
getContext('2d');
const context: CanvasRenderingContext2D
context.CanvasRect.clearRect(x: number, y: number, w: number, h: number): void
clearRect(0, 0, let canvas: {
width: number;
height: number;
getContext(type: "2d", options?: CanvasRenderingContext2DSettings): CanvasRenderingContext2D;
}
canvas.width: number
width, let canvas: {
width: number;
height: number;
getContext(type: "2d", options?: CanvasRenderingContext2DSettings): CanvasRenderingContext2D;
}
canvas.height: number
height);
// これは `color` が変更されるたびに再実行されます...
const context: CanvasRenderingContext2D
context.CanvasFillStrokeStyles.fillStyle: string | CanvasGradient | CanvasPattern
fillStyle = let color: string
color;
function setTimeout<[]>(callback: () => void, ms?: number): NodeJS.Timeout (+2 overloads)
Schedules execution of a one-time callback
after delay
milliseconds.
The callback
will likely not be invoked in precisely delay
milliseconds.
Node.js makes no guarantees about the exact timing of when callbacks will fire,
nor of their ordering. The callback will be called as close as possible to the
time specified.
When delay
is larger than 2147483647
or less than 1
, the delay
will be set to 1
. Non-integer delays are truncated to an integer.
If callback
is not a function, a TypeError
will be thrown.
This method has a custom variant for promises that is available using timersPromises.setTimeout()
.
setTimeout(() => {
// ...しかしこれは `size` が変更されても再実行されません
const context: CanvasRenderingContext2D
context.CanvasRect.fillRect(x: number, y: number, w: number, h: number): void
fillRect(0, 0, let size: number
size, let size: number
size);
}, 0);
});
effect は、読み取るオブジェクトそのものが変更された場合にのみ再実行され、その中のプロパティが変更された場合には再実行されません。(オブジェクト内部の変更を開発時に観察したい場合は、$inspect
を使用できます。)
<script>
let state = $state({ value: 0 });
let derived = $derived({ value: state.value * 2 });
// これは一度だけ実行されます。なぜなら `state` は再代入されていないからです (変異(mutated)のみです)
$effect(() => {
state;
});
// これは `state.value` が変更されるたびに実行されます...
$effect(() => {
state.value;
});
// ...これも `state.value` が変更されるたびに実行されます。なぜなら `derived` は毎回新しいオブジェクトになるからです
$effect(() => {
derived;
});
</script>
<button onclick={() => (state.value += 1)}>
{state.value}
</button>
<p>{state.value} doubled is {derived.value}</p>
effect は、前回実行されたときに読み取られた値のみに依存します。これは、条件付きコードを持つ effect にとって興味深い含意があります。
例えば、以下のコードスニペットで a
が true
の場合、if
ブロック内のコードが実行され、b
が評価されます。そのため、a
または b
の変更が effect を再実行を引き起こします。
一方で、a
が false
の場合、b
は評価されず、 effect は a
が変更されたときに のみ 再実行されます。
function $effect(fn: () => void | (() => void)): void
namespace $effect
Runs code when a component is mounted to the DOM, and then whenever its dependencies change, i.e. $state
or $derived
values.
The timing of the execution is after the DOM has been updated.
Example:
$effect(() => console.log('The count is now ' + count));
If you return a function from the effect, it will be called right before the effect is run again, or when the component is unmounted.
Does not run during server side rendering.
$effect(() => {
var console: Console
The console
module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
- A
Console
class with methods such as console.log()
, console.error()
and console.warn()
that can be used to write to any Node.js stream.
- A global
console
instance configured to write to process.stdout
and
process.stderr
. The global console
can be used without calling require('console')
.
Warning: The global console object’s methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the note on process I/O
for
more information.
Example using the global console
:
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console
class:
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to stdout
with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to printf(3)
(the arguments are all passed to util.format()
).
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
See util.format()
for more information.
log('running');
if (let a: false
a) {
var console: Console
The console
module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
- A
Console
class with methods such as console.log()
, console.error()
and console.warn()
that can be used to write to any Node.js stream.
- A global
console
instance configured to write to process.stdout
and
process.stderr
. The global console
can be used without calling require('console')
.
Warning: The global console object’s methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the note on process I/O
for
more information.
Example using the global console
:
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
Example using the Console
class:
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to stdout
with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to printf(3)
(the arguments are all passed to util.format()
).
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
See util.format()
for more information.
log('b:', let b: false
b);
}
});
$effect.pre
稀なケースでは、DOM 更新の 前に コードを実行する必要がある場合があります。そのためには $effect.pre
rune を使用できます:
<script>
import { tick } from 'svelte';
let div = $state();
let messages = $state([]);
// ...
$effect.pre(() => {
if (!div) return; // まだマウントされていない場合
// `messages` 配列の length が変わるたびにこのコードを再実行するため、ここで参照する
messages.length;
// 新しい message が追加されるたびに自動でスクロールする
if (div.offsetHeight + div.scrollTop > div.scrollHeight - 20) {
tick().then(() => {
div.scrollTo(0, div.scrollHeight);
});
}
});
</script>
<div bind:this={div}>
{#each messages as message}
<p>{message}</p>
{/each}
</div>
タイミングを除けば、$effect.pre
は $effect
とまったく同じように機能します。
$effect.tracking
$effect.tracking
rune は高度な機能で、コードが effect やテンプレートなどの tracking context の中でで実行されているかどうかを示します (デモ):
<script>
console.log('in component setup:', $effect.tracking()); // false
$effect(() => {
console.log('in effect:', $effect.tracking()); // true
});
</script>
<p>in template: {$effect.tracking()}</p> <!-- true -->
これは、createSubscriber
のような抽象化を実装するために使用されます。これにより、リアクティブな値を更新するリスナーが作成されますが、それらの値が追跡されている場合に のみ 実行されます (例えば、イベントハンドラー内で読み取られる場合は除きます)。
$effect.root
$effect.root
rune は高度な機能で、自動クリーンアップされない非トラッキングスコープ (non-tracked scope) を作成します。これは、手動で制御したいネストされた effect に便利です。また、この rune は、コンポーネント初期化フェーズの外部で effect を作成することも可能にします。
<script>
let count = $state(0);
const cleanup = $effect.root(() => {
$effect(() => {
console.log(count);
});
return () => {
console.log('effect root cleanup');
};
});
</script>
$effect を使うべきでないとき
一般的に、$effect
は脱出口 (エスケープハッチ) のようなものと考えるのが最善です。たとえば、アナリティクス や直接的な DOM 操作などに便利ですが、頻繁に使用するツールではありません。特に、state を同期するために使用するのは避けてください。このように使用するのではなく...
<script>
let count = $state(0);
let doubled = $state();
// don't do this!
$effect(() => {
doubled = count * 2;
});
</script>
...このようにしてください:
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>
count * 2
のような単純な式よりも複雑なものの場合は、$derived.by
を使用することもできます。
effect を使ってある値を別の値に関連付けるような複雑な処理をしたくなるかもしれません。次の例は、「支出金額(money spent)」と「残高(money left)」という2つの入力が互いに連動していることを示しています。一方を更新すると、もう一方もそれに応じて更新されるべきです。このようなケースで effect を使用しないでください (デモ):
<script>
let total = 100;
let spent = $state(0);
let left = $state(total);
$effect(() => {
left = total - spent;
});
$effect(() => {
spent = total - left;
});
</script>
<label>
<input type="range" bind:value={spent} max={total} />
{spent}/{total} spent
</label>
<label>
<input type="range" bind:value={left} max={total} />
{left}/{total} left
</label>
代わりに、可能な場合はコールバックを使用してください (デモ):
<script>
let total = 100;
let spent = $state(0);
let left = $state(total);
function updateSpent(e) {
spent = +e.target.value;
left = total - spent;
}
function updateLeft(e) {
left = +e.target.value;
spent = total - left;
}
</script>
<label>
<input type="range" value={spent} oninput={updateSpent} max={total} />
{spent}/{total} spent
</label>
<label>
<input type="range" value={left} oninput={updateLeft} max={total} />
{left}/{total} left
</label>
何らかの理由でバインディングを使用する必要がある場合 (例えば “書き込み可能な $derived
” が必要な場合)、getter と setter を使用して state を同期することを検討してください (デモ):
<script>
let total = 100;
let spent = $state(0);
let left = {
get value() {
return total - spent;
},
set value(v) {
spent = total - v;
}
};
</script>
<label>
<input type="range" bind:value={spent} max={total} />
{spent}/{total} spent
</label>
<label>
<input type="range" bind:value={left.value} max={total} />
{left.value}/{total} left
</label>
どうしても effect 内で $state
を更新する必要があり、同じ $state
を読み書きすることで無限ループに陥った場合は、untrack を使用してください。