
Developer, administrator
Quick share for a use case of Svelte's reactivity in building web extensions with wxt.dev and the extension storage API
Recently I joined a project that needed to build some web extensions. My mind immediately went: "time to finally try wxt.dev for real!". After a long time admiring from afar, I now can officially say I have tried WXT and I'm enjoying it so far. Disclaimer, however, I am by no stretch an expert in either WXT or web extensions. I may very well make some terrible mistake in this post. Please call me out if you see something wrong!
This post essentially shows my attempt to wrap around @wxt-dev/storage
with createSubscriber to make it reactive in Svelte land. Pre-exposure to createSubscriber
is helpful to understand the code here (if not yet, now may be the time?). And it'll also be helpful if you have worked with any of the Web Storage APIs, specifically the @wxt-dev/storage implementation for web extension.
import { createSubscriber } from 'svelte/reactivity';
import { storage } from '#imports'; // assuming @wxt-dev/storage is preinstalled in wxt.dev
export const AUTHENTICATED_KEY = 'local:authenticated';
export class ReactiveStorage {
#authenticated: boolean | null;
#subscribeToAuthenticated: ReturnType<typeof createSubscriber>;
constructor(init: { authenticated: boolean | null }) {
this.#authenticated = init.authenticated;
this.#subscribeToAuthenticated = createSubscriber((update) => {
const unwatch = storage.watch<string>(AUTHENTICATED_KEY, (value) => {
this.#authenticated = value;
update();
});
return unwatch;
});
}
get authenticated() {
this.#subscribeToAuthenticated();
return this.#authenticated;
}
set authenticated(value: boolean | null) {
storage.setItem<boolean>(AUTHENTICATED_KEY, value);
}
}
The code above is quite verbose and only handles the authenticated
item. It will get longer as I add more items to the storage. An abstraction on top to automate the subscription and getter/setter setup for each item is certainly possible. I'm yet to find a need for that, however, as abstraction does also mean less flexibility. Plus I'm too lazy to work out the necessary Typescript generics for this :-).
Say I have a popup entrypoint that needs to use ReactiveStorage
within it, I can further wrap it in a Svelte context to avoid prop drilling:
import { mount } from 'svelte';
import { storage } from '#imports'; // assuming @wxt-dev/storage is preinstalled in wxt.dev
import { ReactiveStorage, AUTHENTICATED_KEY } from '@/reactive-strorage.ts';
import Popup from './popup.svelte';
(async function() {
mount(Popup, {
target: document.getElementById('app')!,
context: new Map([
[
'storage',
new ReactiveStorage({
authenticated: await storage.getItem<boolean>(AUTHENTICATED_KEY),
}),
],
]),
});
})();
<script lang="ts">
import { getContext } from 'svelte';
import type { ReactiveStorage } from '@/reactive-storage.ts';
const storage = getContext<ReactiveStorage>('storage');
</script>
<p>
{#if storage.authenticated}
Authenticated
{:else}
Not authenticated
{/if}
</p>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Your Extension</title>
<meta name="manifest.type" content="browser_action" />
</head>
<body>
<div class="contents" id="app"></div>
<script type="module" src="./popup.ts"></script>
</body>
</html>
The same setup is needed for each UI entry point in my extension.
Taking advantage of storage reactivity is also a viable alternative to passing message around, between content script <-> background <-> popup, for example.
getItem
might be asyncNote in a previous code block, the initial authenticated
value is passed into ReactiveStorage
instance at initialization...
const reactiveStorage = new ReactiveStorage({
authenticated: await storage.getItem<boolean>(AUTHENTICATED_KEY),
});
...and not handled internally within ReactiveStorage
. This is my personal choice, but also because storage.getItem
, in this particular implementation I'm using, is async
. Getting values inside constructor is doable but then we won't know for sure when the fields are populated, hence some possible flash of content. I prefer to have the values ready immediately when ReactiveStorage
is instantiated.
This async behavior is also why I use an intermediate private #authenticated
field instead of calling storage.getItem
directly in the getter:
class ReactiveStorage {
get authenticated() {
this.#subscribeToAuthenticated();
return this.#authenticated;
}
}
Lastly, note the async Immediately-Invoked-Function-Expression (IIFE) in popup/popup.ts
to avoid top-level await, which might not be supported in all browser versions:
(async function() {
//...
const authenticated = await storage.getItem<boolean>(AUTHENTICATED_KEY);
// ...
})();
Your mileage may vary, check your storage implementation and do what feels right for you!
ReactiveStorage
may work in other contexts other than Svelte files. However, I currently only have the need for it inside Svelte land. In scripts, I'd just use the storage
API directly. But if you come up with some cool use case, please let me know!
I hope this has been helpful. The implementation I shared is just a few days old, and I got too excited too fast. You can find me at vnphanquang on Bluesky or via our Svelte Vietnam Discord. Any feedback is well appreciated.
Svelte 5 has been a true step-up for productivity in my daily work. Couple that with wxt.dev and the Vite ecosystem today, I can't really ask for more. To the people that maintain these frameworks, you rock! Go give them support if you can!
Found a typo or need to correct something? Edit this blog post content on Github
Managing CSS icons with Iconify & Tailwind V4, and SVG icons with @svelte-put/inline-svg in Vite & Svelte apps
The Svelte Vietnam Blog Newsletter
Subscribe to receive notification for new blog post from Svelte Vietnam
Edit this page on Github sveltevietnam.dev is an open source project and welcomes your contributions. Thank you!