Reputation: 655
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).
Upvotes: 0
Views: 122
Reputation: 8345
I haven't looked too closely at your code (so I can be wrong), but you basically want to store cookie info from a user in a global variable on the server-side? This is problematic, since the server can receive requests from multiple different users at the same time, so if the server ever performs an asynchronous operation, you may need to remember cookie info from multiple users at the same.
Obtaining the cookie info in hooks.server.ts
is OK, but you can't store it in a global variable. Instead, you rather need to pass along it (for example as an argument) to all the functions you call/objects you create from there, so they can do asynchronous work and still have access to the cookie info from the right user.
But you can also consider to not have server-side rendering support for the pages that uses user-specific information like this.
cookieManager
on the server-sideOn the server-side, to get access to your cookieManager
in handlers and load()
functions, create your cookieManager
and assign it to event.locals in your first handler:
$lib/cookie-manager.js
export class CookieManager {
cookies = null
constructor(cookies){
this.cookies = cookies
}
log(){
console.log(`Cookies`, this.cookies)
}
}
src/hooks.js
import { CookieManager } from '$lib/CookieManager.js';
/** @type {import('@sveltejs/kit').Handle} */
function addCookieManagerHandle({ event, resolve }) {
// This way, the cookie manager becomes available
// in all other handles and load() functions.
event.locals.cookieManager = new CookieManager(event.cookies)
return await resolve(event);
}
export const handle: Handle = sequence(addCookieManagerHandle, ...otherstuff);
You can also add the following to get type info:
src/app.d.ts
declare class CookieManager {
constructor(cookies: any);
log(): void;
}
declare namespace App {
interface Locals {
cookieManager: CookieManager;
}
}
cookieManager
on the client-sideOn the client-side, create the cookieManager
in the root +layout.svelte
file, and use the Context API to make it available to other Svelte components:
src/routes/+layout.svelte
<script>
import { CookieManager } from '$lib/CookieManager.js';
import { setContext } from 'svelte';
setContext(
'cookieManager',
new CookieManager(document.cookie),
);
</script>
<slot />
(since the client-side and the server-side works with different sources for the cookies, it's probably a good idea to have a ServerSideCookieManger
and a ClientSideCookieManager
(same interface, but different implementations))
src/routes/+page.svelte
<script>
import { getContext } from 'svelte';
const cookieManager = getContext(
'cookieManager',
);
</script>
<slot />
cookieManager
on the server-side in Svelte componentsWhen using server-side rendering, I'm guessing you can't use document.cookie
in Svelte components to obtain the user's cookies. So a workaround here is to pass the cookies from event.cookies
to the root +layout.svelte
component using the load()
function, and then using the Context API to make it available to other Svelte components:
src/routes/+layout.server.js
/** @type {import('./$types').LayoutServerLoad} */
export async function load(event) {
return {
cookies: event.cookies.getAll(),
};
}
src/routes/+layout.svelte
<script>
import { CookieManager } from '$lib/CookieManager.js';
import { setContext } from 'svelte';
/** @type {import('./$types').LayoutData} */
export let data;
setContext(
'cookieManager',
new CookieManager(data.cookies), // A third source of cookies, so maybe you need a third implementation of CookieManager to work with this source.
);
</script>
<slot />
Upvotes: 0