63

Is there any direct option to persist svelte store data so that even when the page is refreshed, data will be available.

I am not using local storage since I want the values to be reactive.

Anil Sivadas
  • 1,658
  • 3
  • 16
  • 26

13 Answers13

84

You can manually create a subscription to your store and persist the changes to localStorage and also use the potential value in localStorage as default value.

Example

<script>
  import { writable } from "svelte/store";
  const store = writable(localStorage.getItem("store") || "");

  store.subscribe(val => localStorage.setItem("store", val));
</script>

<input bind:value={$store} />
Tholle
  • 108,070
  • 19
  • 198
  • 189
  • 6
    This works properly in svelte. What is the recommended way of using this in Sapper. I created a separate JS file as below import { writable, derived } from 'svelte/store'; export const name = writable(localStorage.getItem("store") ||'world'); name.subscribe(val => localStorage.setItem("store", val)); But this not running in sapper as localStorage is not available in server – Anil Sivadas Jun 07 '19 at 10:27
  • 6
    @AnilSivadas Doing it on the server complicates it a bit. You could skip it on the server and just do it in the browser with a `typeof window !== 'undefined'` check before using localStorage. – Tholle Jun 07 '19 at 11:04
  • 2
    There is a similar / same example described [here](https://stackoverflow.com/q/56636764/1785391), including the solution (similar as @Tholle described) by using `{#if process.browser}`. – taffit Apr 15 '20 at 14:17
  • Another interesting option is to use `derived()`, but that will make you have double the amount of stores which is usually unnecessary. – elquimista May 05 '20 at 22:00
  • In general, you should not manually subscribe to stores unless you also make sure to unsubscribe. In components it is not necessary either, just use: `$: localStorage.setItem("store", $store);` – H.B. Nov 17 '22 at 10:59
16

From https://github.com/higsch/higsch.me/blob/master/content/post/2019-06-21-svelte-local-storage.md by Matthias Stahl:

Say we have a store variable called count.

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

export const count = writable(0);

// App.svelte
import { count } from 'store.js';

In order to make the store persistent, just include the function useLocalStorage to the store object.

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

const createWritableStore = (key, startValue) => {
  const { subscribe, set } = writable(startValue);
  
  return {
    subscribe,
    set,
    useLocalStorage: () => {
      const json = localStorage.getItem(key);
      if (json) {
        set(JSON.parse(json));
      }
      
      subscribe(current => {
        localStorage.setItem(key, JSON.stringify(current));
      });
    }
  };
}

export const count = createWritableStore('count', 0);

// App.svelte
import { count } from 'store.js';

count.useLocalStorage();

Then, in your App.svelte just invoke the useLocalStorage function to enable the persistent state.

This worked perfectly for me in Routify. For Sapper, JHeth suggests "just place count.useLocalStorage() in onMount or if (process.browser) in the component consuming the store. "

mic
  • 1,190
  • 1
  • 17
  • 29
  • 1
    For others coming across this post and looking for the source: the blog seems to not exist anymore, just the source at github: [https://github.com/higsch/higsch.me/blob/master/content/post/2019-06-21-svelte-local-storage.md](https://github.com/higsch/higsch.me/blob/master/content/post/2019-06-21-svelte-local-storage.md). However @mic posted the whole code here already. Also be aware that if you use sapper, you need to take care if it is run on the server or browser. – taffit May 28 '20 at 21:49
  • 3
    To make it work in Sapper specifically just place `count.useLocalStorage()` in `onMount` or `if (process.browser)` in the component consuming the store. – JHeth Sep 25 '20 at 04:18
15

For Svelte Kit I had issues with SSR. This was my solution based on the Svelte Kit FAQ, the answer by Matyanson and the answer by Adnan Y.

As a bonus this solution also updates the writable if the localStorage changes (e.g. in a different tab). So this solution works across tabs. See the Window: storage event

Put this into a typescript file e.g. $lib/store.ts:

import { browser } from '$app/env';
import type { Writable } from 'svelte/store';
import { writable, get } from 'svelte/store'

const storage = <T>(key: string, initValue: T): Writable<T> => {
    const store = writable(initValue);
    if (!browser) return store;

    const storedValueStr = localStorage.getItem(key);
    if (storedValueStr != null) store.set(JSON.parse(storedValueStr));

    store.subscribe((val) => {
        if ([null, undefined].includes(val)) {
            localStorage.removeItem(key)
        } else {
            localStorage.setItem(key, JSON.stringify(val))
        }
    })

    window.addEventListener('storage', () => {
        const storedValueStr = localStorage.getItem(key);
        if (storedValueStr == null) return;

        const localValue: T = JSON.parse(storedValueStr)
        if (localValue !== get(store)) store.set(localValue);
    });

    return store;
}

export default storage

This can be used like this:

import storage from '$lib/store'

interface Auth {
    jwt: string
}

export const auth = storage<Auth>("auth", { jwt: "" })
Spenhouet
  • 6,556
  • 12
  • 51
  • 76
  • 1
    Work's like magic =) – Victor Hugo Bueno Sep 20 '21 at 22:13
  • Thanks for the full code. Just wondering why is the statement `if (storedValueStr == null) return;` needed? Because by the time the `storage` event listener runs, this key should already be existing in localStorage. – Ammar Sep 30 '21 at 06:42
  • @Ammar I did run into this case. So there seems to be a scenario where it is not existing. – Spenhouet Sep 30 '21 at 10:26
  • Isn't `[null, undefined].includes(val)` strictly equivalent to `val == null`? (I see later a loose comparison with `null` so just wondering if it could be rewritten for consistency without change in behavior.) – sleighty Dec 14 '21 at 01:26
  • 1
    What about the compare at the end `f (localValue !== get(store))`? Wouldn't that fail if the value was an (nested) object? – A. Rabus Mar 02 '22 at 17:33
  • thanks for the code @Spenhouet, may be it need to be updated because I get error `Argument of type 'T' is not assignable to parameter of type 'null | undefined'.` in this line `if ([null, undefined].includes(val)) ` – MSH Apr 22 '22 at 22:19
4

In case someone needs to get this working with JavaScript objects:

export const stored_object = writable(
    localStorage.stored_object? JSON.parse(localStorage.stored_object) : {});
stored_object.subscribe(val => localStorage.setItem("stored_object",JSON.stringify(val)));

The benefit is that you can access the writable object with the $ shorthand, e.g.

<input type="text" bind:value={$stored_object.name}>
<input type="text" bind:value={$stored_object.price}>
Leon
  • 356
  • 6
  • 10
4

TLDR: Here is a function that takes care of not only setting and getting, but also deletion.

function persistent(name) {
    const value = writable(localStorage.getItem(name));
    value.subscribe(val => [null, undefined].includes(val) ? localStorage.removeItem(name) : localStorage.setItem(name, val));
    return value;
}


export const my_token = persistent('token');

Reasoning: Contrary to intuition, localStorage.setItem('someval', null) would not set return null for the next localStorage.getItem('someval') but "null" which is likely not what one would want. Thus, this also checks for undefined and null and deletes the item accordingly.

Adnan Y
  • 2,982
  • 1
  • 26
  • 29
  • I really like the concept of deleting the value in localStorage when set to null. I see how to use the exported `my_token.set("hello")` but it not clear on how to use that function to `get` the value from the my_token.js store function. I can see the value "hello" in the browser dev tools --> Applications --> Local Storage screen, but your words are __Here is a function that takes care of not only setting and getting, but also deletion.__ I'm just not understanding how the `get()` works here.. Note: `my_token.set(null);` works great to delete the value in LocalStorage. Where is `.get()` – zipzit Dec 18 '21 at 06:37
  • oops. `import { get } from "svelte/store";` Would you be offended if I proposed an edit to your code that showed it in use ? – zipzit Dec 18 '21 at 10:03
4

Found a library called svelte-local-storage-store that implements this functionality. Worked for me.

Example from their README:

// in store.ts or similar
import { writable } from 'svelte-local-storage-store'

// First param `preferences` is the local storage key.
// Second param is the initial value.
export const preferences = writable('preferences', {
  theme: 'dark',
  pane: '50%',
  ...
})


// in views

import { get } from 'svelte/store'
import { preferences } from './stores'

preferences.subscribe(...) // subscribe to changes
preferences.update(...) // update value
preferences.set(...) // set value
get(preferences) // read value
$preferences // read value with automatic subscription
fbjorn
  • 722
  • 12
  • 27
2

You may want to also check this one out https://github.com/andsala/svelte-persistent-store

Also, if you use sapper and don't want something to run on the server, you can use the onMount hook

onMount(() => {
  console.log('I only run in the browser');
});
Dmitry Boychev
  • 151
  • 1
  • 8
2

This function synchronises svelte store with localStorage. If there is no value stored it takes the initValue parameter instead.

I also added Typescript.

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

const wStorage = <T>(key: string, initValue: T): Writable<T> => {
    const storedValueStr = localStorage.getItem(key);
    const storedValue: T = JSON.parse(storedValueStr);

    const store = writable(storedValueStr != null ? storedValue : initValue);
    store.subscribe((val) => {
        localStorage.setItem(key, JSON.stringify(val));
    })
    return store;
}

export default wStorage;

You can then use the function elsewhere like you are used to with writable:

const count = wStorage<number>('count', 0);

Edit: If you are using SSR in your app and don't want to use onMount or check if (process.browser) for every writable method. Here is a modified version:

const wStorage = <T>(key: string, initValue: T): Writable<T> => {
    const store = writable(initValue);
    if (typeof Storage === 'undefined') return store;

    const storedValueStr = localStorage.getItem(key);
    if (storedValueStr != null) store.set(JSON.parse(storedValueStr));

    store.subscribe((val) => {
        localStorage.setItem(key, JSON.stringify(val));
    })
    return store;
}
Matyanson
  • 301
  • 2
  • 8
  • 1
    Wouldn't this cause a memory leak? The subscription is never unsubscribed – Jahir Aug 01 '21 at 17:21
  • @Jahir The data saved in localStorage won't be removed but also no more data will be saved. Only the fixed number of values you specify in your app will by saved, no more data will be accumulated over time. The value paired with a key will be overwritten, not added. – Matyanson Aug 02 '21 at 10:57
  • 1
    I understand that. But my question was that the explicit subscription is never unsubscribed. So, isn't there a risk of the subscriptions never getting released and causing memory leaks? – Jahir Aug 02 '21 at 12:44
  • @Jahir That depends on where you call the `wStorage` function. How many times you call it, that many times is the subscription initialized. I use the `wStorage` in `src/store.ts` file, just how it is in the [docs](https://svelte.dev/tutorial/auto-subscriptions). I believe the code runs there only once, am I missing something? If you call the `wStorage` function in component, feel free to modify it (e.g. returning `[store, unsubscribe]` and then using `onDestroy(unsubscribe);` in the component). – Matyanson Aug 02 '21 at 14:54
  • 1
    @Jahir when you create a store using Writable, svelte will take care of the subscriptions/unsubscriptions for you - you just need to prefix your store with $ when referencing it in svelte files. – supafiya Oct 06 '21 at 12:25
2

With svelte 3.38 and svelte-kit (Sapper's succesor) , I use:

<script>
  import { onMount } from 'svelte';
  import { writable } from "svelte/store";

  let value;

  onMount(() => {
    value = writable(localStorage.getItem("storedValue") || "defaut value");
    value.subscribe(val => localStorage.setItem("storedValue", val));
  })
</script>

<input bind:value={$value} />

localStorage isn't available out of onMount()

europrimus
  • 53
  • 5
1

You may do it like this:

import { writable } from 'svelte/store';
import { browser } from '$app/environment';

// check if the item exists in local storage, if so, return the item, otherwise, return null. (This is to avoid errors on initial reads of the store)
// browser && makes sure the command only works in the client side (browser).
const get_local_storage =
    browser && localStorage.getItem('presisted_local_store')
        ? browser && localStorage.getItem('presisted_local_store')
        : null;
// create a writable store
export const presisted_local_store = writable(JSON.parse(get_local_storage));
// create a subscribe method for the store to write back to the local storage (again, on the browser)
presisted_local_store.subscribe((value) => {
    browser && localStorage.setItem('presisted_local_store', JSON.stringify(value));

General Grievance
  • 4,555
  • 31
  • 31
  • 45
0

Works for me with svelte version 3.44.1.

src/store.js file:

import { writable } from "svelte/store";
import { browser } from "$app/env"

export const fontSize = writable(browser && localStorage.getItem("fontSize") || "15");
fontSize.subscribe((value) => {
    if (browser) return localStorage.setItem("fontSize", value)
});

tarasinf
  • 796
  • 5
  • 17
0

copied this code from one of my projects $lib/savable.ts

import type { Writable, StartStopNotifier, Unsubscriber } from 'svelte/types/runtime/store';
import { writable } from 'svelte/store';

const attach = (writable: Writable<unknown>, key='store'): void =>{
    const json = localStorage.getItem(key);
    if (json) {
       writable.set(JSON.parse(json));
    }

    writable.subscribe(current => {
        localStorage.setItem(key, JSON.stringify(current));
    });
}
interface Savable<T> extends Writable<T> {
    mount(localstore: Storage): void
    dismount(localstore: Storage): JSON
    unsub: Unsubscriber
}
function savable<T>(key: string, value?: T, start?: StartStopNotifier<T>): Savable<T>{
    const base = writable(value, start)
    return {
        ...base,
        mount(localstore) {
            if(this.mounted) throw new Error("Already mounted");
            this.mounted = true;

            const json = localstore.getItem(key);
            if (json) {
                base.set(JSON.parse(json));
            }

            this.unsub = base.subscribe(current => {
                localStorage.setItem(key, JSON.stringify(current));
            });
            console.log(this)
        },
        dismount(localstore) {
            if(!this.mounted) throw new Error("Not mounted");
            const json = JSON.parse(localstore.getItem(key))
            this.unsub()
            localstore.removeItem(key)
            return json
        },
        unsub() {
           throw new Error('Cannot unsubscribe when not subscribed')
        }
    }
}
export {
    attach,
    savable,
};
export type {
    Savable
}
export default savable

here is an example of a savable being used in index.svelte

<!—- Typescript is not required  —->
<script lang=ts>
    import savable from `$lib/savable`;
    const value = savable(‘input_value’);
    import { onMount } from ‘svelte’;
    onMount(()=>{
        value.mount()
    })
</script>

<input bind:value={$value}></input>
mavdotjs
  • 65
  • 6
0

In SvelteKit the official method is called Snapshot:

https://kit.svelte.dev/docs/snapshots

Vytenis
  • 21
  • 5