When should I use derived in Svelte custom stores?

Viewed 3293

I've followed along with the great Svelte tutorial but I'm having trouble understanding when I should use derived in my custom stores. In this example, I'm creating a game with 3 states:

  • PRE_GAME
  • IN_GAME
  • POST_GAME

I want to return a boolean check for when I'm in one of those states, and I think it would be best on the custom store itself.

This is my current code:

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

export const gamestate = (() {
  const { set, subscribe } = writable('PRE_GAME');

  return {
    subscribe,
    set
  };
})();

export const preGame = derived(gamestate, ($gamestate) => $gamestate === 'PRE_GAME');
export const inGame = derived(gamestate, ($gamestate) => $gamestate === 'IN_GAME');
export const postGame = derived(gamestate, ($gamestate) => $gamestate === 'POST_GAME');

Is it possible to move the derived methods onto the gamestate store like gamestate.preGame()? Does that make sense to do in Svelte? Then I can call $gameState and get whichever value, but also return a boolean check when I need an explicit value.

I expected to be able to do check the internal value without needing to derive its value. Maybe something like below, but it always returns false because gamestate is a writable object.

export const createStore = (() {
  const { set, subscribe } = writable('PRE_GAME');

  return {
    subscribe,
    set,
    preGame: () => gamestate === 'PRE_GAME',
    inGame: () => gamestate === 'IN_GAME',
    postGame: () => gamestate === 'POST_GAME',
  };
})();

What am I misunderstanding here?

3 Answers

What you do is return a subscription to a derived store in your custom store:

import { derived, writable } from 'svelte/store'

export const gameState = (() => {
  const store = writable('PRE_GAME')
  const store2 = derived(store, $store => ({
    preGame: $store === 'PRE_GAME',
    inGame: $store === 'IN_GAME',
    postGame: $store === 'POST_GAME'        
  }))
    
  return {
    set: store.set,
    subscribe: store2.subscribe
  }
})()

Now when you do set on the store it will set the value on store, then store2 will derive new values and the subscribers will react to those changes:

<script>
  import { gameState } from "./store.js"
</script>

<button on:click="{() => $gameState='PRE_GAME'}">go PRE_GAME</button>
<button on:click="{() => $gameState='IN_GAME'}">go IN_GAME</button>
<button on:click="{() => $gameState='POST_GAME'}">go POST_GAME</button>

<br />

{#if $gameState.preGame}
    <span>PRE_GAME</span>
{/if}

{#if $gameState.inGame}
    <span>IN_GAME</span>
{/if}

{#if $gameState.postGame}
    <span>POST_GAME</span>
{/if}

Alternative

You can also define different getters for each specific state:

import { derived, writable } from 'svelte/store'

export const gameState = (() => {
  const store = writable('PRE_GAME')
    const { set, subscribe } = store

  return {
    set,
    subscribe,
        get preGame() { return derived(store, $store => $store === 'PRE_GAME') },
        get inGame() { return derived(store, $store => $store === 'IN_GAME') },
        get postGame() { return derived(store, $store=> $store === 'POST_GAME') },
  }
})()

Now the only problem is that doing $gameState.preGame does not exists because that tries to get the preGame prop from the content of $gameState, and doing gameState.$preGame or similar is invalid syntax. To get around that you can destructure the props out of the store where you need them:

<script>
  import { gameState } from './store.js'
  const { preGame, inGame, postGame } = gameState
</script>

<span>Current State: {$gameState}</span>
{#if preGame}<span>PRE_GAME</span>{/if}
{#if inGame}<span>IN_GAME</span>{/if}
{#if postGame}<span>POST_GAME</span>{/if}

Custom methods on the stores are for how you mutate the state (set and update), but stores generally are only have one getter: the state. And I don't think you want it to be otherwise.

From here on, I see three solutions : 

  • Make your state an object { gameSate: 'PRE_GAME', isPreGame: true, isInGame: false, isPostGame: false } and then use a custom update function.
  • Use derived stores as you did (I see no point in importing derived in the main store or using get()).
  • Make that calculation in the components, possibly making a dedicated component. Your components would still purely declarative and the only thing mutating still be in the store.

I would decide between just by pure DRYness: imports vs. repeating the clause.

With derived store you can easily have additional variables or values, which are reactive in svelte component. Reactivity can be achieved without derived store, but it’ll be a hacky and ugly solution.

Here is an example of ”forced” reactivity:

store.js:

  function createStore() {
      const { set, subscribe } = writable('PRE_GAME');
      let state
      subscribe((v)=>state=v)
      return {
        subscribe,
        set,
        preGame: () => gamestate === 'PRE_GAME',
        inGame: () => state === 'IN_GAME',
        postGame: () => gamestate === 'POST_GAME',
      };
    }

App.svelte:

<script>
  import {gamestate} from "./store.js"
  $: st=gamestate.inGame($gamestate)
  $: console.log("state", st) 
</script>

<h1 on:click={()=>$gamestate="IN_GAME"}>Hello {$gamestate}!</h1>
<p>state: {st}</p>

Some explanations:
state -variable holds store status and it’s updated everytime the store updates, hence we need to subscribe in to our own store. inGame-function returns is the state ”IN_GAME” or not.

In app-component we need to create a variable st and because it’s created in component it will be reactive. But it’s value is evaluated only by marking it to be observed. Observeable line is made by $:, but there is a catch: you need also give the svelte something to observe and therefore there is $gamestore-variable as a function parameter. Function don’t need it, but now svelte knows to update this line everytime the $gamestore updates.
Here is a REPL:

EDIT: I have updated the REPL to include also the solution mentioned in comment

https://svelte.dev/repl/2991d70689674f50a5fb3e30216d699e?version=3.24.1

You should definitely use a derived store for this kind of reactivity. Derived stores are updated everytime the store, which it is derived from, updates.
But there is also a catch in derived stores. The derived store must be used in component if you want it to be updated ie. you must use somewhere in some svelte component $derived_store (auto-subscription) or subscribe manually to derived store

Related