In svelte, I'm trying to create a module that that gives you an Storage api that is the same for ssr and csr. For instance you can't use localStorage in a server-side component, so calling that method during ssr is basically a NO-OP.
This is the current implementaion
import { browser } from '$app/environment';import Cookies from 'js-cookie';type CookieManager = { set: (key: string, value: string, opts: { expires?: Date; path: string }) => void; get: (key: string) => string | undefined;};/** * the purpose of this module to be ssr compliant of storage types that differ in ssr/csr */export class StorageTypes { static _cookieManager: CookieManager | null = null; /** * nothing is httpOnly here because its a shared module between ssr/csr */ static get cookies() { if (!this._cookieManager && browser) { this._cookieManager = { get: Cookies.get, set: Cookies.set }; } if (!this._cookieManager) { throw new Error('accessing StorageTypes.cookies before assigning it a value'); } return this._cookieManager; } static set cookieManager(cookieManager: CookieManager) { this._cookieManager = cookieManager; } static get localStorage() { return browser ? window.localStorage : { // eslint-disable-next-line @typescript-eslint/no-unused-vars getItem(key: string) { return null; }, // eslint-disable-next-line @typescript-eslint/no-unused-vars setItem(key: string, value: string | null) {} }; }}
For localStorage its straight forward, during SSR I just do nothing, but for cookies, we need to have access to the current response stream to be able to add them. In svelte this can only be done in server-load functions and hooks.server.ts
(as far as I know). But since I need it in this module, this is what I've done (in hooks.server.ts):
export const setServerSideCookieManager: Handle = async ({ event, resolve }) => { StorageTypes.cookieManager = { set: event.cookies.set, get: event.cookies.get }; return await resolve(event);};export const handle: Handle = sequence(setServerSideCookieManager, ...otherstuff);
This will add the current cookie manager that svelte gives us for the current request. But this is the worst solution someone could comeup with (I mean me). What I want to know is that if ther are any better ways to achieve what I'm trying to do (or if this is good enough?).
If you are wondering this is my use case:
function cookie$<T extends ObjectStorageTypes>(key: string, options?: Options<T>) { const storage = StorageTypes.cookies.get(key); const parsed: T = storage ? JSON.parse(storage) : options?.default; let reactiveValue = $state<T>(parsed ?? options?.initializer); $effect.root(() => { $effect(() => { const expirationDate = new Date(Date.now()); expirationDate.setSeconds( expirationDate.getSeconds() + parseInt(PUBLIC_COOKIES_EXPIRATION_SPAN_SECONDS) ); StorageTypes.cookies.set(key, JSON.stringify(reactiveValue), { expires: expirationDate, path: '/' }); }); }); return { get value$(): T { return reactiveValue; }, set value$(newValue: T) { reactiveValue = newValue; } };}
This gives us an object that by changing it, it will automatically update the cookie (I freaking love svelte bro)! For example we can call it like
Persisted.cookie$<{ value: Theme }>(THEME_COOKIE_KEY, { initializer: { value: 'dark' }});
I expected to have a way to access the server-side cookie in more idiomatic way (we can simply check if we are in server-side by using $browser
variable).