$effect
Les effets permettent à votre application de faire des choses. Lorsque Svelte exécute une fonction
d’effet, il détermine les états (et d’états dérivés) qui sont lus, (à moins qu’ils ne soient
exemptés par untrack
), et ré-exécute la fonction lorsque ces états changent.
La plupart des effets dans une application Svelte sont créés par Svelte lui-même — c’est ce qui
permet de mettre à jour le texte dans <h1>coucou {name} !</h1>
lorsque que name
change, par
exemple.
Mais vous pouvez aussi créer vos propres effets avec la rune $effect
, ce qui sert lorsque vous
avez besoin de synchroniser un système extérieur (que ce soit une librairie, un élément
<canvas>
, ou quelque chose sur le réseau) avec un état au sein de votre application Svelte.
Évitez de trop vous servir d’
$effect
! Lorsque trop de choses sont gérés via des effets, le code devient difficile à comprendre et à maintenir. Voir la partie quand ne pas utiliser$effect
pour en savoir plus sur les moyens de les éviter.
Vos effets sont exécutés après le montage du composant dans le DOM, et lors d’une micro-tâche juste après la mise à jour de l’état (démo).
<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);
// ceci va être rejoué lorsque `color` ou `size` change
context.fillStyle = color;
context.fillRect(0, 0, size, size);
});
</script>
<canvas bind:this={canvas} width="100" height="100" />
Les ré-exécutions sont mutualisées (i.e. changer color
et size
au même moment ne va pas
déclencher deux ré-exécutions distinctes), et se produisent après que les éventuelles modifications
du DOM aient eu lieu.
Vous pouvez places des $effect
n’importe où, pas uniquement à la racine d’un composant, tant qu’il
est exécuté lors de l’initialisation du composant (ou lorsqu’un effet parent est actif). Il est
ensuite attaché au cycle de vie du composant (ou de l’effet parent) et sera donc détruit lorsque du
démontage du composant (ou lorsque l’effet parent est détruit).
Vous pouvez renvoyer une fonction depuis $effect
, qui sera immédiatement exécutée avant la
ré-exécution de l’effet, et lorsque celui-ci est détruit
(démo)
<script>
let count = $state(0);
let milliseconds = $state(1000);
$effect(() => {
// ceci sera recréé lorsque `milliseconds` change
const interval = setInterval(() => {
count += 1;
}, milliseconds);
return () => {
// si un callback est fourni, il sera rejoué
// a) immédiatement avant que l'effet ne se rejoue
// b) lorsque que le composant est démonté
clearInterval(interval);
};
});
</script>
<h1>{count}</h1>
<button onclick={() => (milliseconds *= 2)}>ralentir</button>
<button onclick={() => (milliseconds /= 2)}>accélérer</button>
Comprendre les dépendances
$effect
détecte automatiquement toute valeur réactive ($state
, $derived
, $props
) qui est lue
de manière synchrone à l’intérieur de son corps de fonction et l’enregistre en tant que
dépendance. Lorsque ces dépendances changent, l’$effect
va planifier une ré-exécution.
Les valeurs qui sont lues de manière asynchrones — après un await
ou à l’intérieur d’un
setTimeout
, par exemple — ne seront pas considérées comme dépendances. Ici, le canvas sera repeint
lorsque color
change, mais pas lorsque size
change
(démo) :
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);
// cet effet sera rejoué lorsque `color` change...
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(() => {
// ... mais pas lorsque `size` change
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);
});
Un effet est seulement rejoué lorsque l’objet qu’il lit change, pas lorsqu’une propriété de cet
objet change. (If vous souhaitez observer les changement à l’intérieur d’un objet lors de vos
développements, vous pouvez utiliser $inspect
.)
<script>
let state = $state({ value: 0 });
let derived = $derived({ value: state.value * 2 });
// ceci sera joué une seule fois, car `state` n'est jamais réassigné (seulement muté)
$effect(() => {
state;
});
// ceci sera rejoué à chaque fois que `state.value` change...
$effect(() => {
state.value;
});
// ... et ceci également, car `derived` est un nouvel objet à chaque fois
$effect(() => {
derived;
});
</script>
<button onclick={() => (state.value += 1)}>
{state.value}
</button>
<p>Le double de {state.value} vaut {derived.value}</p>
Un effet dépend uniquement des valeurs qu’il a lues la dernière fois qu’il a été joué. Ceci a des conséquences intéressantes pour les effets qui impliquent du code conditionnel.
Par exemple, si a
vaut true
dans le code ci-dessous, le code dans le bloc #if
sera joué et b
sera ré-évalué. Dans ce cas, n’importe quel changement sur a
ou b
va déclencher la ré-exécution
de
l’effet.
À l’inverse, si a
vaut false
, b
ne sera pas ré-évalué, et l’effet ne sera rejoué que si a
change.
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('effet');
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
Dans de rares cas, vous pourriez avoir besoin d’exécuter votre code avant que le DOM ne soit mis à
jour. Pour cela, nous avons accès à la rune $effect.pre
:
<script>
import { tick } from 'svelte';
let div = $state();
let messages = $state([]);
// ...
$effect.pre(() => {
if (!div) return; // pas encore monté
// référence à la longueur du tableau `messages` afin que ce code soit rejoué lorsque celle-ci
// change
messages.length;
// scroll automatique lorsque de nouveaux messages sont ajoutés
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>
Le timing mis à part, $effect.pre
fonctionne exactement comme $effect
.
$effect.tracking
La rune $effect.tracking
est une fonctionnalité avancée qui vous informe de si le code est exécuté
au sein d’un contexte de suivi (tracking), comme un effet ou bien dans le template
(démo)
:
<script>
console.log('lors de la mise en place du composant :', $effect.tracking()); // false
$effect(() => {
console.log('dans l\'effet :', $effect.tracking()); // true
});
</script>
<p>dans le template: {$effect.tracking()}</p> <!-- true -->
C’est notamment utilisé pour implémenter des abstractions comme
createSubscriber
, qui servent à créer des
gestionnaires permettant de mettre à jour des valeurs réactives seulement si ces valeurs sont
trackées (plutôt que, par exemple, simplement lues dans un gestionnaire d’évènement).
$effect.root
La rune $effect.root
est une fonctionnalité avancée qui crée un scope non suivi qui ne
s’auto-nettoie pas. Cela est utile pour les effets imbriqués que vous souhaitez contrôler
manuellement. Cette rune permet également la création d’effets en dehors de la phase
d’initialisation du composant.
<script>
let count = $state(0);
const cleanup = $effect.root(() => {
$effect(() => {
console.log(count);
});
return () => {
console.log('nettoyage de l\'effet racine');
};
});
</script>
Quand ne pas utiliser $effect
En général, $effect
est plutôt considéré comme un dernier recours — pratique pour des choses comme
les analytics ou la manipulation directe du DOM — plutôt qu’un outil que vous devriez utiliser
souvent. En particulier, évitez de vous en servir pour synchroniser un état. Plutôt que faire
ceci...
<script>
let count = $state(0);
let doubled = $state();
// ne faites pas ça !
$effect(() => {
doubled = count * 2;
});
</script>
... faites ceci :
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>
Pour des choses plus compliquées qu’une simple expression comme
count * 2
, vous pouvez aussi utiliser$derived.by
.
Vous pourriez être tenté•e de faire des choses tordues avec les effets pour lier une valeur à une autre. L’exemple suivant montre deux inputs pour “argent dépensé” et “argent restant” qui sont connectés l’un à l’autre. Si vous en mettez un à jour, l’autre s’ajuste automatiquement. N’utilisez pas d’effets pour faire ça (démo.
<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} dépensé
</label>
<label>
<input type="range" bind:value={left} max={total} />
{left}/{total} restant
</label>
Utilisez plutôt des callbacks lorsque c’est possible (démo).
<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} dépensé
</label>
<label>
<input type="range" value={left} oninput={updateLeft} max={total} />
{left}/{total} restant
</label>
Si vous avez besoin d’utiliser des liaisons, peu importe la raison (par exemple lorsque vous voulez
quelque chose comme un $derived
que l’on peut modifier manuellement), envisagez l’utilisation de
getters et setters pour synchroniser l’état
(démo).
<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} dépensé
</label>
<label>
<input type="range" bind:value={left.value} max={total} />
{left.value}/{total} restant
</label>
Si vous devez absolument mettre à jour un $state
dans un effet et que cela déclenche une boucle
infinie parce que vous lisez et écrivez un même $state
, utilisez untrack.