Skip to main content

subscribeKey Tutorial

subscribeKey() subscribes to changes on a single property of a store proxy. It's the property-level equivalent of subscribe() — instead of firing on every store mutation, it only fires when the specific key you're watching actually changes.

Getting Started

1. Create a store

import {createClassyStore} from '@codebelt/classy-store';

class SettingsStore {
theme: 'light' | 'dark' = 'light';
fontSize = 14;
language = 'en';

setTheme(theme: 'light' | 'dark') {
this.theme = theme;
}

setFontSize(size: number) {
this.fontSize = size;
}
}

export const settingsStore = createClassyStore(new SettingsStore());

2. Watch a single key

import {subscribeKey} from '@codebelt/classy-store/utils';

const unsub = subscribeKey(settingsStore, 'theme', (value, previousValue) => {
console.log(`Theme changed from "${previousValue}" to "${value}"`);
document.documentElement.setAttribute('data-theme', value);
});

Now only theme changes trigger the callback. Mutations to fontSize or language are ignored entirely.

3. Unsubscribe when done

unsub();

Signature

function subscribeKey<T extends object, K extends keyof T>(
proxyStore: T,
key: K,
callback: (value: Snapshot<T>[K], previousValue: Snapshot<T>[K]) => void,
): () => void;
ParameterTypeDescription
proxyStoreTA reactive proxy created by createClassyStore()
keykeyof TThe property to watch
callback(value, previousValue) => voidCalled when the watched property changes
Returns() => voidUnsubscribe function

How It Works

Under the hood, subscribeKey wraps the existing subscribe() + snapshot() APIs:

  1. Captures an initial snapshot and stores snapshot[key] as the previous value.
  2. On each store mutation, takes a new snapshot and compares snapshot[key] with the previous value via Object.is().
  3. If different, fires the callback with (currentValue, previousValue) and updates the stored previous value.

Because snapshot() uses structural sharing, unchanged sub-trees return the same reference. This makes the Object.is() comparison efficient — no deep equality checks are needed.

Use Cases

Reacting to auth state changes

class AuthStore {
token: string | null = null;
user: { name: string; role: string } | null = null;

login(token: string, user: { name: string; role: string }) {
this.token = token;
this.user = user;
}

logout() {
this.token = null;
this.user = null;
}
}

const authStore = createClassyStore(new AuthStore());

// Redirect on login/logout
subscribeKey(authStore, 'token', (token, previousToken) => {
if (token && !previousToken) {
router.push('/dashboard');
} else if (!token && previousToken) {
router.push('/login');
}
});

Syncing with external systems

class PlayerStore {
volume = 0.8;
track: string | null = null;
playing = false;

setVolume(v: number) {
this.volume = v;
}

play(track: string) {
this.track = track;
this.playing = true;
}
}

const playerStore = createClassyStore(new PlayerStore());

// Sync volume slider with the Web Audio API
subscribeKey(playerStore, 'volume', (volume) => {
audioContext.gainNode.gain.value = volume;
});

// Update document title when track changes
subscribeKey(playerStore, 'track', (track) => {
document.title = track ? `Playing: ${track}` : 'Music Player';
});

Logging specific changes

class CartStore {
items: { id: string; qty: number }[] = [];
coupon: string | null = null;

get total() {
return this.items.reduce((sum, item) => sum + item.qty, 0);
}

addItem(id: string) {
this.items.push({ id, qty: 1 });
}

applyCoupon(code: string) {
this.coupon = code;
}
}

const cartStore = createClassyStore(new CartStore());

// Only log when coupon changes — not on every item add
subscribeKey(cartStore, 'coupon', (coupon, prev) => {
analytics.track('coupon_changed', { from: prev, to: coupon });
});

subscribeKey vs subscribe

subscribe()subscribeKey()
Fires onAny mutation in the storeOnly when the watched key changes
Callback args() (no arguments)(value, previousValue)
Use caseGeneral side effectsProperty-specific reactions
OverheadMinimalOne snapshot comparison per mutation

Use subscribe() when you need to react to any change. Use subscribeKey() when you only care about one property and want to avoid running your callback unnecessarily.

Multiple Keys

To watch multiple keys independently, call subscribeKey once per key:

const unsub1 = subscribeKey(store, 'theme', handleThemeChange);
const unsub2 = subscribeKey(store, 'language', handleLanguageChange);

// Clean up both
function dispose() {
unsub1();
unsub2();
}

Each subscription is independent — they don't interfere with each other.

Quick Reference

What you wantHow to do it
Watch a single propertysubscribeKey(store, 'key', (val, prev) => { ... })
Stop watchingconst unsub = subscribeKey(...); unsub()
Watch multiple propertiesCall subscribeKey() once per key
Get both old and new valueCallback receives (value, previousValue)