observable
@bedrock-core/observable is the reactive primitive the stack notifies through: a value with three verbs — get / set / subscribe — the same three a config leaf has, a shared key has, a db document has, and Minecraft's own data-driven UI observables have.
@bedrock-core/observable is in beta: the API can change between releases. Pin exact versions and read the changelog before upgrading.
What is @bedrock-core/observable?
Every accessor in the server stack is one of these, so what you learn here applies to config.server.taxRate, shared.price and balances.for(player) alike, and the derived forms — computed, effect, last — compose with all of them.
Pure TypeScript. Nothing in the main entry imports the engine, so it runs in vitest exactly as it runs in a realm. The one bridge to @minecraft/server-ui is a separate entry, @bedrock-core/observable/minecraft.
Install
- npm
- yarn
- pnpm
npm install @bedrock-core/observable
yarn add @bedrock-core/observable
pnpm add @bedrock-core/observable
Or reach it through the meta package, which pins a matching version:
import { observable, computed, effect, batch, last, toNative } from '@bedrock-core/server/observable';
@minecraft/server-ui (>=2.1.0) is an optional peer dependency, needed only by toNative.
Usage
import { observable, computed, effect, batch, last } from '@bedrock-core/observable';
const phase = observable<'lobby' | 'fight' | 'end'>('lobby');
const alive = observable(new Set<string>());
const aliveCount = computed(() => alive.get().size, [alive]);
effect(() => bossBar.setTitle(phase.get()), [phase]); // runs now and on every change
aliveCount.subscribe((next, prev) => scoreboard.set(next));
batch(() => { // one notification per observable
phase.set('end');
alive.set(new Set());
});
const spawn = last(world.afterEvents.playerSpawn); // undefined, then each payload
What you get
- Synchronous — a listener runs inside the
setthat changed the value, in the same tick, so what you read after a write is what every listener has already seen.batchis the only thing that defers, and it delivers at its end, in order. - Immutable values,
Object.isby default —setreplaces; an observable of an object gets a new object. Passequalsto change what counts as a change, so a listener never fires for a value that did not. - Listed dependencies —
computedandeffecttake their dependencies explicitly: no proxy, no tracking, no cost on reads. - Isolated listeners — one that throws is reported with the observable's
labeland skipped; the rest still run, so one screen's bug cannot stop another's update. ReadonlyObservable<T>— whatcomputedreturns and what anything exposing a value it owns hands out, so a consumer cannotsetwhat is not theirs.last— an event as a value: the most recent payload,undefinedbefore the first, from any signal withsubscribe.
Next steps
observable— a value withget,setandsubscribecomputed— a read-only value derived from otherseffect— run something now and whenever a dependency changesbatch— several writes, one notification eachlast— an event's most recent payload as a valuetoNative— bind one to a data-driven form's own observable