How to persist svelte store
Asked Answered
H

13

65

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.

Hogback answered 7/6, 2019 at 5:0 Comment(1)
Using Snapshots you may not need a store at all depending on your use case.Maintop
P
86

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} />
Peracid answered 7/6, 2019 at 7:30 Comment(5)
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 serverHogback
@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.Peracid
There is a similar / same example described here, including the solution (similar as @Peracid described) by using {#if process.browser}.Tezel
Another interesting option is to use derived(), but that will make you have double the amount of stores which is usually unnecessary.Tetrapterous
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);Stressful
P
17

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: "" })
Prudhoe answered 14/8, 2021 at 16:13 Comment(7)
Work's like magic =)Showmanship
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.Immanuel
@Immanuel I did run into this case. So there seems to be a scenario where it is not existing.Prudhoe
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.)Fowling
What about the compare at the end f (localValue !== get(store))? Wouldn't that fail if the value was an (nested) object?Cullet
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)) Viscera
@Immanuel , @Prudhoe it might be getting triggered when the key is removed. Look into Mozilla docs for key event propertyElla
D
15

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. "

Duntson answered 19/4, 2020 at 7:15 Comment(2)
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. However @Duntson 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.Tezel
To make it work in Sapper specifically just place count.useLocalStorage() in onMount or if (process.browser) in the component consuming the store.Gadolinite
A
6

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

Install:

npm install svelte-persisted-store

Example from their README:

// in store.ts or similar
import { persisted } from 'svelte-persisted-store'

// First param `preferences` is the local storage key.
// Second param is the initial value.
export const preferences = persisted('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
Aileen answered 6/7, 2022 at 10:21 Comment(0)
U
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}>
Undergarment answered 1/2, 2021 at 15:3 Comment(0)
T
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.

Trisoctahedron answered 2/5, 2021 at 5:3 Comment(2)
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()Flamenco
oops. import { get } from "svelte/store"; Would you be offended if I proposed an edit to your code that showed it in use ?Flamenco
M
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');
});
Mike answered 8/3, 2020 at 6:6 Comment(0)
D
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;
}
Danford answered 19/5, 2021 at 19:8 Comment(5)
Wouldn't this cause a memory leak? The subscription is never unsubscribedCalondra
@Calondra 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.Danford
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?Calondra
@Calondra 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. 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).Danford
@Calondra 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.Moralist
K
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()

Kev answered 22/7, 2021 at 7:53 Comment(1)
I've just created a svelte/vite project with svelte 3.38 : it seems that localStorage is available outside onMount()Estheresthesia
B
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));

Bundelkhand answered 16/1, 2023 at 18:31 Comment(0)
R
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)
});

Rootlet answered 13/11, 2021 at 11:51 Comment(0)
C
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>
Carlenacarlene answered 16/5, 2022 at 19:55 Comment(0)
A
0

In SvelteKit the official method is called Snapshot:

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

Algie answered 14/4, 2023 at 18:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.