Search code examples
javascripttypescriptlocal-storagesvelte

Svelte variable not updating from localStorage without using setInterval and forcing it


I have an app that stores items into locations in localStorage and then displays the items in HTML.

One of the reasons i wanted to use Svelte was for reactive variables, but whenever I attempt to use a reactive variable that changes whenever localStorage.current_items changes, the ItemList variable doesn't change.

The only way I could get it to work is by using setInterval but that is not a great way to do it. How can I make it so that ItemList changes properly when the localStorage.current_items string changes.

<script lang="ts">
    import {
        getData,
        createItem,
        createLocation,
        closeItem,
    } from './lib/database.js';
    import LocationSelector from './lib/LocationSelector.svelte';
    import { flip } from 'svelte/animate';
    import { writable } from 'svelte/store';

    let DB = getData();
    // load items from localstorage.items

    let ItemList = [];
    let Location;

    setInterval(() => {
        Location = localStorage.current_items;
        ItemList = JSON.parse(localStorage.current_items).items;
    }, 500);

    console.log(ItemList);

    let newItem = '';
    let filter_showClosed = false;

    function addNewItem(e) {
        e.preventDefault();
        console.log(newItem);
        const newItemInput = document.querySelector(
            '#newItemInput'
        ) as HTMLInputElement;

        createItem(JSON.parse(Location).id, newItem);
        newItem = '';
    }

    function newItemKeyDown(e) {
        if (e.keyCode === 13) {
            addNewItem(e);
        }
    }
</script>

<LocationSelector />

<div class="app">
    <input
        type="text"
        id="newItemInput"
        bind:value={newItem}
        placeholder="Add a new item"
        on:keydown={newItemKeyDown}
    />
    <button
        id="filter_showClosed"
        data-active="false"
        on:click={function () {
            filter_showClosed = !filter_showClosed;
            let el = document.getElementById('filter_showClosed');
            if (filter_showClosed) {
                el.innerHTML = 'Hide closed';
                el.dataset.active = 'true';
            } else {
                el.innerHTML = 'Show closed';
                el.dataset.active = 'false';
            }
        }}>Show closed</button
    >
    <!-- <button
        id="deleteClosed"
        on:click={function () {
            let it = items;
            for (let i = 0; i < it.length; i++) {
                if (it[i].closed == true) {
                    it.splice(i, 1);
                }
            }
            items = it;
            sort_items(items);
        }}>Delete all closed</button
    > -->

    <div class="list">
        {#each ItemList as item, index (item.id)}
            <div class="item {item.closed}" animate:flip={{ duration: 100 }}>
                {#if item.closed == false || (filter_showClosed == true && item.closed == true)}
                    <div>
                        <img
                            src="/up.svg"
                            class="item-icon"
                            class:closed={item.closed == true}
                            alt="move item up in priority"
                            on:click={function () {
                                // increaseLevel({ item });
                            }}
                        />
                        {item.name} ({index})
                    </div>
                    <div>
                        {#if item.closed == false}
                            <img
                                src="/close.svg"
                                class="item-icon"
                                alt="close item"
                                on:click={function () {
                                    console.log(Location.id);
                                    closeItem(JSON.parse(Location).id, item.id);
                                }}
                            />
                        {/if}
                    </div>
                {/if}
            </div>
        {/each}
    </div>
</div>

<style>
</style>

I tried using this writeable method, but that didn't work either as the variable still didn't change.

import { writable } from 'svelte/store';

const ItemList = writable([]);
let Location = {};

let newItem = '';
let filter_showClosed = false;

function addNewItem(e) {
    e.preventDefault();
    console.log(newItem);
    const newItemInput = document.querySelector(
        '#newItemInput'
    ) as HTMLInputElement;

    createItem(Location.id, newItem);
    newItem = '';
}

function newItemKeyDown(e) {
    if (e.keyCode === 13) {
        addNewItem(e);
    }
}

// Update the Location object with the current value of localStorage.current_items as an object
Location = JSON.parse(localStorage.current_items);

// Update the ItemList store with the new location's items
ItemList.set(Location.items);


Solution

  • You should use a store that fully wraps the access to localStorage.

    Something like:

    function localStorageStore(key, initial) {
        const value = localStorage.getItem(key)
        const store = writable(value == null ? initial : JSON.parse(value));
        store.subscribe(v => localStorage.setItem(key, JSON.stringify(v)));
        
        return store;
    }
    

    Reading and writing is just a regular store, but on initial load the value comes from the storage and on setting the value, it is also written to storage.


    In Svelte 5 you can use a $state with an $effect.

    To make it easy to pass around the value, even if it is a primitive, it should be wrapped (see e.g. also the svelte/reactivity/window module).

    function localStorageState(key, initial) {
        const value = localStorage.getItem(key)
        const state = $state({
            current: value == null ? initial : JSON.parse(value)
        });
    
        $effect(() => {
            localStorage.setItem(key, JSON.stringify(state.current));
        });
    
        return state;
    }
    
    <script>
        // ...
        const name = localStorageState('name', 'world');
    </script>
    
    <h1>Hello {name.current}!</h1>
    <input bind:value={name.current} />
    

    (If the function is defined in a JS/TS file, the name has to include .svelte., so e.g. local-storage.svelte.js.)


    These are sketch implementations for demonstration of the principles only. They e.g. do not handle SSR, which has no access to localStorage and have no built-in way of handling version changes (if you want to change the format of the value later, which is stored on each client and thus not easily accessible).