Skip to main content

useObservable

Reads an observable, and keeps the component's copy of it current. A change lands the way a useState change does.

Import​

import { useObservable } from '@bedrock-core/ui';

Signature​

function useObservable<T>(source: ObservableLike<T>): T;
function useObservable<T, S>(source: ObservableLike<T>, select: (value: T) => S): S;

Parameters​

ParameterTypeDefaultDescription
source*ObservableLike<T>—Anything with get() and subscribe(listener)
select(value: T) => S—Narrows what the component depends on, so only a change to the slice counts

Returns​

The current value, or the selected slice of it.

What counts as an observable​

ObservableLike<T> is structural: get() for the current value, subscribe(listener) for the next ones, returning an unsubscribe.

interface ObservableLike<T> {
get(): T;
subscribe(listener: (next: T, prev: T) => void): () => void;
}

@bedrock-core/observable's ReadonlyObservable, a config leaf, a db document and a query all have that shape.

Usage​

const phase = useObservable(phaseObs);

Examples​

Only react to a slice​

function PlayerCount(): JSX.Element {
const count = useObservable(playersObs, players => players.size);

return <Text maxLength={3}>{String(count)}</Text>;
}

A change to the collection that leaves its size alone schedules nothing. Equality is Object.is, applied by the state slot.

A config value​

With config the accessor register() handed back:

function Currency(): JSX.Element {
const symbol = useObservable(config.server.economy.currency);

return <Text maxLength={4}>{symbol}</Text>;
}

Notes​

A change from the observable is a state change, so when the player sees it depends on the screen:

ScreenA change
FormIs kept, and shows on the next screen the player's press brings up: an open form cannot change
Container screenUpdates the screen's live values at once: a maxLength text, a <List> count, a carried visible

What the build baked stays as the build drew it on both. See State.

select is read through a ref rather than a dependency, so passing an inline arrow — the usual way to write one — does not resubscribe on every render. The subscription is keyed on the observable alone.

The value can move between a render and the subscription landing, so the hook reads once more before listening. An unchanged value costs nothing.