Cours Svelte et SvelteKit · Module 7: Cycle de vie
Mises à jour du DOM : $effect.pre, $effect et tick()
Dans cette leçon5
Parfois, il faut réagir juste avant que le DOM change ou juste après - par exemple pour faire défiler le chat des guerriers jusqu'au dernier message. Svelte 5 propose deux runes pour cela.
$effect.pre - avant la mise à jour du DOM
$effect.pre s'exécute avant la mise à jour du DOM, chaque fois que l'état qu'il lit change :
<script>
let messages = $state([]);
$effect.pre(() => {
console.log('Avant la mise à jour du DOM, messages :', messages.length);
});
</script>$effect - après la mise à jour du DOM
Un $effect classique s'exécute après la mise à jour du DOM, il peut donc déjà mesurer et faire défiler les éléments :
<script>
let messages = $state(['Bienvenue au Valhalla !']);
let container;
$effect(() => {
messages.length; // dépendance : l'effet revient après chaque nouveau message
// Défiler vers le bas après l'ajout d'un message
container.scrollTop = container.scrollHeight;
});
</script>
<div bind:this={container} class="messages">
{#each messages as msg}
<p>{msg}</p>
{/each}
</div>
<button onclick={() => messages.push('SKÅL!')}>Ajouter un message</button>La ligne messages.length; se contente de lire l'état, pour que l'effet sache de quoi il dépend. Cet effet ignore les autres changements du composant (comme le changement de thème).
Ne modifie pas l'état qu'un effet lit
Un effet qui lit log et le modifie en même temps se relance, encore et encore... Svelte arrête une telle boucle avec l'erreur effect_update_depth_exceeded. Calcule les valeurs dérivées de l'état avec $derived, et quand un effet doit vraiment écrire quelque chose, enveloppe cette écriture dans untrack :
<script>
import { untrack } from 'svelte';
let count = $state(0);
let log = $state([]);
$effect(() => {
const value = count; // la seule dépendance de l'effet
// untrack : lire log à l'intérieur ne crée pas de dépendance
untrack(() => {
log = [...log, `count = ${value}`];
});
});
</script>
<button onclick={() => count++}>+1</button>
<p>{log.join(', ')}</p>tick() - Attendre la mise à jour du DOM
<script>
import { tick } from 'svelte';
let text = $state('');
let input;
async function addText(newText) {
text += newText;
// Attendre la mise à jour du DOM
await tick();
// Maintenant on peut manipuler le DOM en toute sécurité
input.setSelectionRange(text.length, text.length);
}
</script>
<input bind:this={input} bind:value={text} />
<button onclick={() => addText(' Viking!')}>Ajouter</button>Svelte 4 (legacy) : beforeUpdate et afterUpdate
Dans l'ancien code, tu verras beforeUpdate et afterUpdate. Ils s'exécutaient à chaque mise à jour du composant. En Svelte 5, ils sont obsolètes et ne fonctionnent que dans les composants sans runes : dans un composant avec $state, les importer provoque une erreur de compilation. Remplace-les ainsi :
| Svelte 4 | Svelte 5 |
|---|---|
beforeUpdate(fn) | $effect.pre(fn) |
afterUpdate(fn) | $effect(fn) |
<!-- Chat.svelte - Svelte 4 (legacy) -->
<script>
import { afterUpdate } from 'svelte';
export let messages = [];
let container;
afterUpdate(() => {
container.scrollTop = container.scrollHeight;
});
</script>
<div bind:this={container}>
{#each messages as msg}
<p>{msg}</p>
{/each}
</div>Les effets sont plus précis que les anciens hooks : ils réagissent uniquement à l'état qu'ils utilisent, et non à chaque changement du composant.
Code de cette leçon : App.svelte
<script>
// $effect - nouvelle façon de gérer les effets dans Svelte 5
// Remplace beforeUpdate et afterUpdate (onMount fonctionne toujours)
import { untrack } from 'svelte';
let count = $state(0);
let name = $state('Ragnar');
let logs = $state([]);
// untrack : écrire dans logs ne devient pas une dépendance de l’effet
// qui a appelé log(), sinon l’effet lirait et modifierait logs sans fin
function log(msg) {
untrack(() => {
logs = [...logs, msg].slice(-10);
});
}
// $effect s'exécute quand les dépendances changent
$effect(() => {
log(`Effet : count = ${count}`);
});
$effect(() => {
log(`Effet : name = ${name}`);
});
// $effect avec nettoyage
$effect(() => {
const interval = setInterval(() => {
log('⏰ Tic-tac...');
}, 3000);
// Nettoyage - retourne une fonction
return () => {
clearInterval(interval);
log("Nettoyage de l'intervalle");
};
});
// $effect.pre - avant la mise à jour du DOM
$effect.pre(() => {
if (count > 0) {
log(`⏳ Pré-effet : count sera ${count}`);
}
});
</script>
<main class="effects">
<h1>$effect - Effets réactifs</h1>
<section>
<h2>Compteur</h2>
<p>Compteur : <strong>{count}</strong></p>
<button onclick={() => count++}>+1</button>
</section>
<section>
<h2>Nom</h2>
<input bind:value={name} />
<p>Bonjour, <strong>{name}</strong> !</p>
</section>
<section class="logs">
<h2>Journal des effets</h2>
{#each logs as entry, i}
<p>{entry}</p>
{/each}
</section>
</main>
<style>
.effects {
font-family: 'Georgia', serif;
max-width: 500px;
margin: 0 auto;
padding: 2rem;
background: #1a0f0a;
color: #e8d5b7;
min-height: 100vh;
}
h1 {
color: #d4af37;
text-align: center;
}
section {
background: rgba(212, 175, 55, 0.1);
border: 1px solid #d4af37;
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
}
h2 {
color: #d4af37;
font-size: 1rem;
margin-top: 0;
}
strong {
color: #ffd700;
}
button {
padding: 0.5rem 1rem;
background: #3d2914;
color: #e8d5b7;
border: 1px solid #d4af37;
border-radius: 4px;
cursor: pointer;
}
input {
width: 100%;
padding: 0.5rem;
background: rgba(0,0,0,0.3);
border: 1px solid #3d2914;
border-radius: 4px;
color: #e8d5b7;
margin-bottom: 0.5rem;
}
.logs {
max-height: 200px;
overflow-y: auto;
}
.logs p {
margin: 0.25rem 0;
padding: 0.25rem 0.5rem;
background: rgba(0,0,0,0.3);
border-radius: 4px;
font-size: 0.85rem;
font-family: monospace;
}
</style>Entrées de la documentation utilisées dans cette leçon
Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. À quoi sert la fonction tick() ?
2. Qu'est-ce que $effect.pre dans Svelte 5 ?