svelte/storeについて

Arata Kurokawa
nextbeat-engineering
9 min readMay 31, 2023

ネクストビートでWebエンジニアとして働いている黒川と申します。
ネクストビートは2021年6月入社で、「保育士バンク!」のアプリの開発を行っております。

SvelteとIonicによるアプリの技術移行でリアクティブな実装をする必要がありました。
技術移行前は主にRxJSを使っていたので、svelteで代用できるものがないかと思い、公式ドキュメントを見るとsvelte/store がありましたのでこちらを使用しました。

writableやreadableは仕様がシンプルなので、ここではderivedをどのような場面で使用したか紹介できればと思います。

derivedとは

1つ以上の他のストア(writable, readable)からストアを派生させます。これらの依存しているものが変化するたびに、コールバックが実行されます。

下記は数値を倍にするストアを派生する簡単な例です。

const a = writable(1)
const doubled = derived(
  a,
  $a => $a * 2 // コールバック
)

const unsubscribe = doubled.subscribe(value => {
console.log(`value: ${value}`)
})

// aが変更されることでコールバックが実行されます。
a.set(2)
a.set(3)

// ログ結果
value: 2
value: 4
value: 6

また第1引数に複数ストアを渡すことができます。

const a = writable(1)
const b = writable(1)
const doubled = derived(
[a, b],
[$a, $b] => $a + $b // コールバック
)

const unsubscribe = doubled.subscribe(value => {
console.log(`value: ${value}`)
})

// aが変更されることでコールバックが実行されます。
a.set(2)
b.set(3)

// ログ結果 a + b
value: 2 // 1 + 1
value: 3 // 2 + 1
value: 5 // 2 + 3

コールバックは、第2引数に set を受け取り、しかるべき時にそれを呼び出すことで非同期に値を設定できます。
この場合、derived に第3引数として、set が初めて呼び出される前の派生ストアの初期値を渡すこともできます。

const a = writable(1)
const delayed = derived(
  a,
  // コールバック
  ($a, set) => { 
   if ($a >= 5) {
set($a)
}
  },
  0 // 初期値
)

const unsubscribe = doubled.subscribe(value => {
console.log(`value: ${value}`)
})

a.set(4)
a.set(5)

// ログ結果
// 4の場合はsetが実行されないためログが出力されない
value: 0
value: 5

コールバックから関数を返すと、コールバック再実行、サブスクライブ解除した時に呼ばれます。

const a = writable(1)
const delayed = derived(
  a,
  // コールバック
  ($a, set) => { 
   const timeout = setTimeout(() => set($a), 3000)
     return () => clearTimeout(timeout) // 関数
  },
  0 // 初期値
)

const unsubscribe = doubled.subscribe(value => {
console.log(`value: ${value}`)
})

// 2秒後に実行
// aに変更が発生したためclearTimeoutが実行される
a.set(2)

// ログ結果
value: 0
value: 2 // ← 5秒後に表示

使用例

  1. 一定時間、スクロールしなかった場合の検知
// スクロールが発生した
const scroll: Writable<number> = writable(0)

// 90秒間画面操作がなかったか
const noScroll = derived<Readable<number>, boolean>(
scroll,
(_, set) => {
const sec = 90
const mill = sec * 1000
const timeout = setTimeout(() => { set(true) }, mill)
return () => clearTimeout(timeout)
},
false
)

// 下記を画面でスクロールが発生するたびに実行
scroll.update(current => current + 1)

scrollに変更があるたびにclearTimeoutが呼ばれるので、
0秒からタイマーが再度始まります。

2. 複数条件のいずれかを満たしたかの検知

const condition1: Writable<boolean> = writable(false)
const condition2: Writable<boolean> = writable(false)
const condition3: Writable<boolean> = writable(false)

const condition = derived<
[
Readable<boolean>,
Readable<boolean>,
Readable<boolean>
],
boolean
>(
[condition1, condition2, condition3],
(values, set) => {
const [
condition1Value,
condition2Value,
condition3Value
] = values

// 比較演算子を&&にすると複数条件のすべてを満たしたらという条件にもできます。
if (condition1Value || condition2Value || condition3Value) {
set(true)
}
},
false
)


// 条件を満たしたときに実行する。
condition1.set(true)
condition2.set(true)
condition3.set(true)

第一引数に複数のstoreを渡すこともできます。
condition1~3のいずれかにtrueがセットされれば、conditionにtrueがセットされます。

Viewへの組み込み

<script lang="ts">
const condition1: Writable<boolean> = writable(false)
const condition2: Writable<boolean> = writable(false)
const condition3: Writable<boolean> = writable(false)

const condition = derived<
[
Readable<boolean>,
Readable<boolean>,
Readable<boolean>
],
boolean
>(
[condition1, condition2, condition3],
(values, set) => {
const [
condition1Value,
condition2Value,
condition3Value
] = values

if (condition1Value || condition2Value || condition3Value) {
set(true)
}
},
false
)

let unsubscribe: Unsubscriber

onMount(() => {
unsubscribe = condition.subscribe(async (value) => {
console.log(`value: ${value}`)
})
})

onDestory(() => {
unsubscribe()
})

const onClickButton = () => {
condition1.set(true)
})
</script>

{#if $condition }
<span>条件を満たした</span>
{#else}
<span>条件を満たしていない</span>
{/if}

<button on:click={onClickButton}>button</button>

ストアの先頭に$を付けておくとconditionに変更があった場合、リアクティブに表示を切り替えることができます。

{#if $condition }
<span>条件を満たした</span>
{#else}
<span>条件を満たしていない</span>
{/if}

初期表示は「条件を満たしていない」と表示されていますが、
ボタンをクリックするとcondition1が変更されるので、conditionにtrueがセットされ「条件を満たした」が表示されるようになります。

ここまで読んでいただきありがとうございました!

RxJSみたいな機能(debounce, combinelatest, etc…)はありませんが、ストアを組み合わせたり、derivedのコールバックを使用することで実現することができそうです。

We are hiring!

本記事をご覧いただき、ネクストビートの技術や組織についてもっと話を聞いてみたいと思われた方、カジュアルにお話しませんか?

・今後のキャリアについて悩んでいる
・記事だけでなく、より詳しい内容について知りたい
・実際に働いている人の声を聴いてみたい

など、まだ転職を決められていない方でも、ネクストビートに少しでもご興味をお持ちいただけましたら、ぜひカジュアルにお話しましょう!

🔽申し込みはこちら
https://hrmos.co/pages/nextbeat/jobs/1000008

また、ネクストビートについてはこちらもご覧ください。

🔽エントランスブック
https://note.nextbeat.co.jp/n/nd6f64ba9b8dc

--

--