Reputation: 485
I have an app that uses Svelte (SSR with Astro), and I'm trying to persist my data to localStorage
.
There just isn't enough documentation about this IMO, since I seem to be running into brick walls constantly.
So far I have this store defined:
import { writable } from 'svelte/store'
function createCart() {
const { subscribe, set, update } = writable([]);
return {
subscribe,
update: (item) => update(value => [...value, item]),
remove: (item) => update(n => {
if(n.length > 0) n.pop() // only remove last item
return n // update the store value
}),
empty: () => set([])
};
}
export const cart = createCart()
To save to localStorage
(and not mess up my SSR) I've implemented this:
import { writable } from 'svelte/store'
function createCart() {
const defaultCartValue = []
const isClient = typeof window !== 'undefined'
const initialValue = isClient ? window.localStorage.getItem('cart') : defaultCartValue;
const { subscribe, set, update } = writable(
isClient
? window.localStorage.setItem('cart', initialValue)
: defaultCartValue
);
return {
subscribe: subscribe(value => localStorage.setItem('cart', ([
value,
...initialValue
]))),
update: (value) => isClient
? window.localStorage.setItem(
'cart',
JSON.stringify([...value, item])
)
: update(value => [...value, item]),
// TODO: fix remove items from localStorage
remove: (item => update(n => {
if(n.length > 0) n.pop()
return n
})
),
empty: () => isClient
? window.localStorage.setItem('cart', defaultCartValue)
: set(defaultCartValue)
};
}
export const cart = createCart()
I keep getting Cannot read property 'unsubscribe' of undefined
followed by stop is not a function
.
Can anyone help me understand why I might be seeing those 2 errors?
Upvotes: 3
Views: 3420
Reputation: 25001
The error you're getting:
Cannot read property 'unsubscribe' of undefined followed by stop is not a function.
That's because the subscribe
method of a store has to return an unsubscribe
(aka stop
) function. This is not stressed very much in the docs, but it shows in the signature of the method:
subscribe: (subscription: (value: any) => void) => (() => void)
But your own code doesn't do that and, for what it's worth, it probably doesn't really do what you intended:
subscribe: subscribe(value => localStorage.setItem('cart', ([
value,
...initialValue
]))),
You're calling the original subscribe
immediately when creating the store, as opposed to when a new subscriber is registered. Since the original subscribe
does return an unsubscribe
function, you don't get a "is not a function" error when trying to subscribe. But you don't get a subscription either, and the unsubscribe
function does not return another function, which explains the error you're seeing.
What you want instead is something like that:
subscribe: subscriber => {
// do something on every new subscriber
// subscribing
const unsubscribe = originalSubscribe(value => {
// do something every time the value change FOR EVERY SUBSCRIBER
subscriber(value) // notify the subscriber of the new value
}
// custom unsubscribe function
return () => {
// do something each time a subscriber unsubscribes
unsubcribe() // unsubscribe from the original store
}
}
If you follow the comments in the previous snippet, you should realize that wrapping subscribe
is probably not what you want to persist to localStorage
, because the "hooks" you get this way are for every subscriber, not once per store.
One way to achieve what you want would be to override the set
and update
method of a writable store like svelte-local-storage-store
does:
const {subscribe, set} = store
stores[key] = {
set(value: T) {
updateStorage(key, value)
set(value)
},
update(updater: Updater<T>) {
const value = updater(get(store))
updateStorage(key, value)
set(value)
},
subscribe
}
Another dead simple and conveniently modular way of doing it is simply to subscribe to your store.
// a guard because we only want this in the clients
if (!isSSR() && typeof localStorage !== 'undefined') {
originalStore.subscribe(value => {
localStorage.setItem('key', JSON.stringify(value))
})
}
This only applies if you don't mind having your store "always active" though; that is, you'd probably don't want to do that with a "rich" store that would do a request when it gets its first subscriber, for example.
Yet another way to leverage the original store's subscribe
all while avoiding the pitfall of eagerly subscribing to it, would be to wrap the above logic in a readable store.
const createPersistedStore = initialValue => {
const internal = writable(initialValue)
const persisting = readable(initialValue, set => {
// called when the store becomes active (i.e. first subscriber arrives)
const unsubscribe = internal.subscribe(value => {
updateLocalStorage(value) // <== will see each change once
set(value)
})
return () => {
// called when the store becomes inactive (i.e. last subscriber leaves)
unsubscribe()
}
})
return {
set: internal.set,
update: internal.update,
subscribe: persisting.subscribe,
}
}
Upvotes: 3
Reputation: 485
This isn't going to help me understand svelte stores better, but this NPM module was a quick solution:
https://www.npmjs.com/package/svelte-local-storage-store
🎉
Upvotes: 2