Cours Svelte et SvelteKit · Module 8: Stores

Stores personnalisés

2 min de lecture ·
Dans cette leçon3

Crée tes propres stores avec une logique métier :

Exemple : Store de compteur

// counterStore.js
import { writable } from 'svelte/store';

function createCounter(initial = 0) {
  const { subscribe, set, update } = writable(initial);

  return {
    subscribe,
    increment: () => update(n => n + 1),
    decrement: () => update(n => n - 1),
    reset: () => set(initial),
    set
  };
}

export const counter = createCounter(0);
<script>
  import { counter } from './counterStore.js';
</script>

<p>Count: {$counter}</p>
<button onclick={counter.increment}>+</button>
<button onclick={counter.decrement}>-</button>
<button onclick={counter.reset}>Reset</button>

Exemple : Store de guerriers

// warriorsStore.js
import { writable, derived } from 'svelte/store';

function createWarriorsStore() {
  const { subscribe, set, update } = writable([]);

  return {
    subscribe,
    add: (warrior) => update(warriors => [...warriors, {
      id: Date.now(),
      ...warrior
    }]),
    remove: (id) => update(warriors =>
      warriors.filter(w => w.id !== id)
    ),
    updateStrength: (id, strength) => update(warriors =>
      warriors.map(w => w.id === id ? { ...w, strength } : w)
    ),
    reset: () => set([])
  };
}

export const warriors = createWarriorsStore();

// Stores dérivés
export const totalWarriors = derived(warriors, $w => $w.length);
export const totalStrength = derived(warriors,
  $w => $w.reduce((sum, w) => sum + w.strength, 0)
);

Store avec localStorage

// settingsStore.js
import { writable } from 'svelte/store';

function createPersistentStore(key, initial) {
  // SSR (SvelteKit) : il n'y a pas de localStorage sur le serveur
  const storage = typeof localStorage !== 'undefined' ? localStorage : null;
  const stored = storage?.getItem(key);
  const data = stored ? JSON.parse(stored) : initial;

  const { subscribe, set, update } = writable(data);

  return {
    subscribe,
    set: (value) => {
      storage?.setItem(key, JSON.stringify(value));
      set(value);
    },
    update: (fn) => {
      update(current => {
        const newValue = fn(current);
        storage?.setItem(key, JSON.stringify(newValue));
        return newValue;
      });
    }
  };
}

export const settings = createPersistentStore('settings', {
  theme: 'dark',
  language: 'pl'
});

La vérification typeof localStorage évite une erreur du module pendant le rendu serveur dans SvelteKit : le store y garde sa valeur initiale.

Code de cette leçon : App.svelte
<script>
  // Store personnalisé avec méthodes
  import { warriors, addWarrior, removeWarrior, stats } from './stores.js';

  let newName = $state('');

  function handleAdd() {
    if (newName.trim()) {
      addWarrior(newName);
      newName = '';
    }
  }
</script>

<main class="warriors-app">
  <h1>Store personnalisé</h1>
  <p class="subtitle">Store avec méthodes personnalisées</p>

  <section>
    <h2>Ajouter un guerrier</h2>
    <div class="add-form">
      <input
        bind:value={newName}
        placeholder="Nom du guerrier"
        onkeydown={(e) => e.key === 'Enter' && handleAdd()}
      />
      <button onclick={handleAdd}>Ajouter</button>
    </div>
  </section>

  <section>
    <h2>Liste des guerriers ({$warriors.length})</h2>
    {#each $warriors as warrior (warrior.id)}
      <div class="warrior-row">
        <span>{warrior.name}</span>
        <span class="health">{warrior.health}</span>
        <button onclick={() => removeWarrior(warrior.id)} aria-label="Retirer {warrior.name}">×</button>
      </div>
    {:else}
      <p class="empty">Aucun guerrier</p>
    {/each}
  </section>

  <section class="stats">
    <h2>Statistiques (derived)</h2>
    <div class="stat-grid">
      <div class="stat">
        <span>Total</span>
        <strong>{$stats.total}</strong>
      </div>
      <div class="stat">
        <span>PV total</span>
        <strong>{$stats.totalHealth}</strong>
      </div>
      <div class="stat">
        <span>PV moyens</span>
        <strong>{$stats.avgHealth}</strong>
      </div>
    </div>
  </section>
</main>

<style>
  .warriors-app {
    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;
  }

  .subtitle {
    text-align: center;
    color: #8b7355;
  }

  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;
  }

  .add-form {
    display: flex;
    gap: 0.5rem;
  }

  input {
    flex: 1;
    padding: 0.5rem;
    background: rgba(0,0,0,0.3);
    border: 1px solid #3d2914;
    border-radius: 4px;
    color: #e8d5b7;
  }

  button {
    padding: 0.5rem 1rem;
    background: #3d2914;
    color: #e8d5b7;
    border: 1px solid #d4af37;
    border-radius: 4px;
    cursor: pointer;
  }

  .warrior-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem;
    background: rgba(0,0,0,0.3);
    margin: 0.25rem 0;
    border-radius: 4px;
  }

  .warrior-row span:first-child {
    flex: 1;
  }

  .health {
    color: #ff6b6b;
  }

  .warrior-row button {
    background: #8b0000;
    border-color: #8b0000;
    padding: 0.25rem 0.5rem;
  }

  .empty {
    text-align: center;
    color: #8b7355;
  }

  .stat-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.5rem;
  }

  .stat {
    text-align: center;
    padding: 0.5rem;
    background: rgba(0,0,0,0.3);
    border-radius: 4px;
  }

  .stat span {
    display: block;
    font-size: 0.85rem;
    color: #8b7355;
  }

  .stat strong {
    font-size: 1.25rem;
    color: #d4af37;
  }
</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. 1. Que doit exporter un custom store pour être compatible avec la syntaxe $ ?

Articles utiles