Saturday, September 26, 2026
Announcing QuantaJS 3.0: One Store for Every Framework
Posted by

Announcing QuantaJS 3.0
QuantaJS 3.0.0 is out, and it is the release the name "framework-agnostic"
was always promising.
Until now, @quantajs/core ran anywhere, but React was the only framework with
official bindings. 3.0 adds Vue, Svelte and Lit, and an Astro
integration that lets islands written in different frameworks share one store.
A store you define once now works in all of them, with the same containers,
server rendering and persistence.
It is a major version for two reasons. The public API is smaller: things that
were deprecated, duplicated or internal are gone. And every package now shares
one version number, so the question "which @quantajs/react goes with which
@quantajs/core?" no longer exists. The answer is always "the same one".
One store, five frameworks
Here is a store:
// stores/cart.ts
import { defineStore } from '@quantajs/core';
export const useCart = defineStore('cart', {
state: () => ({ items: [] as string[] }),
getters: { count: (s) => s.items.length },
actions: {
add(item: string) {
this.items.push(item);
},
},
});
And here is the same button in each framework. Every binding has the same shape: read a value through a selector, get actions without subscribing.
import { useQuantaActions, useQuantaValue } from '@quantajs/react';
import { useCart } from './stores/cart';
export function CartButton() {
const count = useQuantaValue(useCart, (s) => s.count);
const cart = useQuantaActions(useCart);
return <button onClick={() => cart.add('tea')}>{count} items</button>;
}
In each of them, the button re-renders when count changes and at no other
time. The selector runs inside QuantaJS's own dependency tracking, so this holds
even when you mutate an array in place.
| Package | For | Guide |
|---|---|---|
@quantajs/react | React 18 and 19, Next.js App Router | React |
@quantajs/vue | Vue 3.3 and later | Vue |
@quantajs/svelte | Svelte 4 and 5, SvelteKit | Svelte |
@quantajs/lit | Lit 2 and 3, web components | Lit |
@quantajs/astro | Astro 7 islands | Astro |
Two of these deserve their own posts:
- Astro. Islands are isolated by design, and Astro's own recipe for sharing
state notes that a store written on the server does not reach client islands.
@quantajs/astrogives each request its own container and carries its state into every island, whatever framework it uses. One store, every island. - Lit. Web components have had no settled answer for shared state.
@quantajs/litbrings stores to them as reactive controllers. State management for web components.
Server state without passing containers around
2.1 introduced containers: one per request, so concurrent requests never share state. The catch was that you had to pass the container everywhere, which falls apart as soon as a framework renders your components for you.
setDefaultContainerResolver lets a server decide which container is the
default for the code running right now:
import { AsyncLocalStorage } from 'node:async_hooks';
import {
createContainer,
setDefaultContainerResolver,
type StoreContainer,
} from '@quantajs/core';
const requests = new AsyncLocalStorage<StoreContainer>();
setDefaultContainerResolver(() => requests.getStore());
export async function handle(request: Request) {
const container = createContainer();
try {
// Anything in here, including components rendered on the server,
// resolves stores against this request's container.
return await requests.run(container, () => render(request));
} finally {
container.dispose();
}
}
Outside a request, the resolver returns undefined and the usual default
applies. The Astro integration is built on exactly this.
Persist to a cookie
CookieAdapter persists small state, such as a theme or locale, in a cookie, so
the server sees it on the next request:
import { CookieAdapter, defineStore } from '@quantajs/core';
export const usePrefs = defineStore('prefs', {
state: () => ({ theme: 'light', recent: [] as string[] }),
persist: {
adapter: new CookieAdapter('prefs', { maxAge: 60 * 60 * 24 * 365 }),
include: ['theme'],
},
});
A cookie holds at most 4096 bytes. A write that would go over throws, and the
store reports it through onError instead of silently keeping stale data. This
adapter came from QuantaJS's first community pull request, by
@fatihcvs. More on that below.
Also new
watchtakesequals, to decide whether a new value counts as a change. Passshallowfor a watcher over a derived object.- Development warnings no longer reach production. Development mode is read
from
process.env.NODE_ENV, which bundlers replace at build time. - React StrictMode:
<QuantaProvider>without a container, anduseLocalStore, now survive StrictMode's simulated unmount, anduseLocalStorere-renders on changes. - Persistence loads only the keys a store persists, so removing a key from
includeactually stops it being restored.
Smaller
The ES build now ships one file per module, so your bundler drops what you do
not import. Every import path shrank by 1.2 to 1.4 KB gzip; reactive plus
effect went from 5.7 KB to 4.4 KB.
What each path costs in a production build, minified and gzipped:
| You import | Size |
|---|---|
reactive + effect | 4.4 KB |
defineStore | 8.6 KB |
defineStore + React hooks | 9.2 KB |
defineStore + Vue composables | 8.8 KB |
defineStore + Svelte stores | 8.8 KB |
defineStore + Lit controllers | 9.1 KB |
These numbers are checked on every pull request, against a budget.
Leaner
@quantajs/core exported 46 runtime values in 2.3; it exports 33 in 3.0,
counting the new ones. Removed:
- Name-based lookup (
useStore(name),hasStore(name)). Call the definition, or usecontainer.get(name). store.notifyAll(),nextTick()andreactiveEffect.- The migration helpers and
createPersistenceManager:persistcovers both. - Internal utilities:
debounce,Logger,createLogger, the tracking and sanitising helpers.loggerandLogLevelstay. - The deprecated type aliases.
Persistence is also typed end to end now: include and exclude accept only
your state's keys, and migrations and validators receive stored data as
unknown values that you check before trusting. When we moved this site to
3.0, those types caught an unchecked access in one of our own demos.
The migration guide covers every removal, with the replacement for each.
How we checked it
A state library asks you to trust it with your data, so here is what backs this release:
- 587 tests, plus type tests for the public API.
- The oldest versions we claim: each framework binding is tested in CI
against the oldest framework version it supports: React 18, Vue 3.3, Svelte 4 and Lit 2. Testing
Vue 3.3 found a real bug before release:
createQuanta()never disposed its container there. - Seven example apps (vanilla, React, Next.js, Vue, Svelte, Lit and Astro) are built and checked on every pull request. The Vue, Svelte and Astro examples fire concurrent requests at a server render and check that none sees another's state.
- The published packages themselves: CI packs each tarball and installs it into a fresh app, the way you would.
- Provenance: every package is published from GitHub Actions with npm provenance, only from a tagged release, and only after a maintainer approves it.
Upgrading
Stores are unchanged: a defineStore from 2.x works as it is. Update every
QuantaJS package together, since they now require each other from the same
major:
npm install @quantajs/core@3 @quantajs/react@3
Add @quantajs/devtools@3 if you use DevTools.
Then follow the migration guide for anything you used that was removed.
Thank you
@fatihcvs sent QuantaJS's first community pull requests during this cycle: the cookie adapter, tests for the web storage adapters, and missing assertions in the core suite. Each came with a clear description of how it was checked. Thank you.
If you would like to contribute too, the good first issues are a friendly place to start, and CONTRIBUTING.md gets you set up.
The full list of changes is in the v3.0.0 release notes.