Skip to content
Guide/Reactivity

Reactivity

The core ships no reactive primitive. QubeeStore exposes two methods, and every framework binds to them differently:

store.getSnapshot(); // the current state — stable identity between writes
store.subscribe(fn); // returns an unsubscribe function

That is deliberately React’s useSyncExternalStore contract, which also happens to be the smallest sensible one. Both are bound to their store, so each can be passed on its own — exactly what useSyncExternalStore does with them.

import { useSyncExternalStore } from 'react';
function usePage(store: QubeeStore) {
return useSyncExternalStore(store.subscribe, store.getSnapshot);
}

getSnapshot() returns the same object until the next write, which is what stops useSyncExternalStore re-rendering forever.

private readonly _state = signal(this.store.getSnapshot());
constructor() {
this.store.subscribe(() => this._state.set(this.store.getSnapshot()));
}
readonly currentPage = computed(() => this._state().page);
const state = shallowRef(store.getSnapshot());
store.subscribe(() => (state.value = store.getSnapshot()));
const stop = store.subscribe(() => console.log(qb.generateUri()));
stop();

The returned state is deeply frozen. Mutating it throws in strict mode rather than silently corrupting a later read:

store.getSnapshot().includes.push('author');
// TypeError: Cannot add property 0, object is not extensible

Two guarantees have to hold at once — a stable identity between writes, and immunity from caller mutation. Freezing is what makes both true.