Search code examples
sveltesvelte-store

Store with custom method and reactive values


I am building a Svelte component to display a list whose items can be added to a selection. The selection itself is a store:

selectionStore.js

import {writable} from 'svelte/store';

function createSelectionStore() {
    const { subscribe, set, update } = writable(JSON.parse(localStorage.getItem("selection")) || {});

    function remove(selection, itemId, itemType) {...}

    function add(selection, item) {...}

    return {
        subscribe,
        remove: (itemId, itemType) => update(selection => remove(selection, itemId, itemType)),
        add: (item) => update(selection => add(selection, item)),
        toggleSelection: (item) => update(selection => {
            if (selection[item.type]?.[item.id]) {
                return remove(selection, item.id, item.type);
            }
            return add(selection, item);
        }),
        isSelected: (selection, item) => {
            return selection[item.type]?.hasOwnProperty(item.id) || false;
        },
        length: (selection, itemType) => {
            return Object.keys(selectedItems[itemType]).length ?? 0
        },
    };
}
export const selectionStore = createSelectionStore();

The store is then imported in the components: RecordList.svelte

<script>
    import Record from "./Record.svelte";
    import { selectionStore } from "./selectionStore.js";
    export let records = [];

    $: selectionLength = selectionStore.length(false);
</script>

<p>Number of selected items: {selectionLength}</p>

{#if records.length !== 0}
    <div>
        {#each records as item (item.id)}
            <Record {item}/>
        {/each}
    </div>
{/if}

Record.svelte

<script>
    import { selectionStore } from './selectionStore.svelte';
    export let item;

    $: isSelected = selectionStore.isSelected(item);
</script>

<div class="item">
    {item.item}
    <button on:click={() => selectionStore.toggleSelection(item)}>
        {isSelected ? 'Remove from' : 'Add to'} selection
    </button>
</div>

The issue I am facing is that isSelected and selectionLength are not reactive, even though toggleSelection() seem to work. I know that I am not structuring my store properly but I cannot find the right way to do it.


Edit

Here what I ended doing, thank you very much for all your help!

selectionStore.js

import {writable} from 'svelte/store';

function createSelectionStore() {
    const { subscribe, set, update } = writable(JSON.parse(localStorage.getItem("selection")) || {});

    function remove(selection, itemId, itemType) {...}

    function add(selection, item) {...}

    return {
        subscribe,
        remove: (itemId, itemType) => update(selection => remove(selection, itemId, itemType)),
        add: (item) => update(selection => add(selection, item)),
        toggleSelection: (item) => update(selection => {
            if (selection[item.type]?.[item.id]) {
                return remove(selection, item.id, item.type);
            }
            return add(selection, item);
        }),
        isSelected: derived(selection, $selection =>
            item => $selection[item.type]?.hasOwnProperty(item.id) || false
        ),
        nbSelected: derived(selection, $selection =>
            itemType => Object.keys(selectedItems[itemType]).length ?? 0
        ),
    };
}
export const selectionStore = createSelectionStore();

RecordList.svelte

<script>
    import Record from "./Record.svelte";
    import { selectionStore } from "./selectionStore.js";
    const { nbSelected } = selectionStore;
    export let records = [];
</script>

<p>Number of selected items: {$nbSelected("Records")}</p>

{#if records.length !== 0}
    <div>
        {#each records as item (item.id)}
            <Record {item}/>
        {/each}
    </div>
{/if}

Record.svelte

<script>
    import { selectionStore } from './selectionStore.svelte';
    const { isSelected } = selectionStore;
    export let item;
</script>

<div class="item">
    {item.item}
    <button on:click={() => selectionStore.toggleSelection(item)}>
        {$isSelected(item) ? 'Remove from' : 'Add to'} selection
    </button>
</div>

Solution

  • Everything that should be reactive needs to be a store.
    In the case of these functions, you need a derived that depends on the store holding the selection data.

    const selection = writable(...);
    const { subscribe, set, update } = selection;
    
    const isSelected = derived(selection, $selection =>
      item => $selection[item.type]?.hasOwnProperty(item.id) || false
    );
    
    ...
    
    return { subscribe, isSelected, ... }
    
    <script>
      import { selectionStore } from './selectionStore.js';
    
      export let item;
      
      const { isSelected } = selectionStore; // required for accessing store via $
      $: itemSelected = $isSelected(item); // or just inline in template
    </script>
    

    REPL example

    (This will be a lot more simple in Svelte 5 with runes rather than stores. Functions that access state will be reactive by default.)