Expressive State
API Reference

Instructions

Field initializers - set, get, ref, def - with every overload

Instructions are special initializers for class fields. They wire up reactive behavior declaratively.

import { set, get, ref, def } from '@expressive/react';

All instructions return a symbol at definition time that the library resolves into a real typed value during activation. TypeScript sees the declared type; the runtime sees the symbol until init.


set

The most versatile instruction. Handles default values, placeholders, lazy/async factories, reactive computed values, and validation callbacks.

Property descriptor policy

FormEnumerableWritableNotes
= "foo" (plain)yesyesNormal data property
= set("foo")noyesManaged default, excluded from snapshots
= set("foo", cb)noyesManaged with setter callback
= set(() => "foo")nonoFactory, read-only
= set(() => "foo", cb)noyesFactory made writable by callback
= set((from) => ...)yesnoReactive computed, included in snapshots
= set<T>()noyesPlaceholder, suspends on access
= set(promise)nonoDirect promise, suspends until resolved

The key distinction between a factory (set(() => v)) and a reactive computed (set((from) => v)) is parameter count. A callback with no declared parameters is a lazy factory. A callback with at least one declared parameter is a reactive computed whose argument is a tracking proxy.

set<T>() - placeholder

userId = set<string>();

Required. Throws Suspense on read until assigned. Writable. Non-enumerable.

Optional setter callback:

userId = set<string>(undefined, (next, prev) => { /* ... */ });

set(value) - default value

name = set('untitled');

Like name = 'untitled' but non-enumerable - excluded from snapshots and ref(this).

set(value, callback) - default with validation

email = set('', (next, prev) => {
  if (!next.includes('@')) throw false; // reject
});

Callback behaviors:

  • throw false - reject. Value does not change. No event.
  • throw true - accept silently. Value changes, no event emitted.
  • Return a function - cleanup, called before next update.
  • Throw an Error - rethrown to caller.
  • Return a promise - ignored.

set(factory) - lazy factory

config = set(() => loadConfig());
data = set(async () => fetchData()); // async, suspends on access

Zero-argument function. Runs on first access. Read-only.

set(factory, true) - eager factory

data = set(() => fetch(), true);

Runs immediately on activation. Still read-only.

set(factory, false) - lazy factory, no suspense

avatar = set(async () => loadAvatar(), false);

Returns undefined while pending instead of suspending.

set(factory, callback) - factory with setter

config = set(
  () => loadDefaults(),
  (next, prev) => { /* ... */ }
);

Makes the property writable. Callback runs on factory resolution and subsequent assignments.

set((from) => ...) - reactive computed

class Cart extends State {
  items = set<Item[]>([]);
  total = set((from) => from.items.reduce((s, i) => s + i.price, 0));
}

Enumerable, read-only. Re-runs when tracked dependencies change.

  • from is a tracking proxy - reads create subscriptions.
  • this (inside the function) does not create subscriptions - use from for tracked reads.
  • Chained computed values evaluate in declaration order.
  • Can reference its own previous value via from.ownProp without looping.
  • Can be a method reference: doubled = set(this.computeDoubled);
  • Included in snapshots and ref(this).

set(promise) - direct promise

config = set(fetchConfig());

Promise is constructed eagerly. Reads suspend until resolved.


get

Context lookup. Fetches another State from the ambient context hierarchy.

get(Type) - required upstream

parent = get(ParentState);

Throws "Could not find {Type} in context." if missing.

get(Type, false) - optional upstream

maybe = get(OptionalSvc, false); // T | undefined

get(Type, callback) - upstream with lifecycle

parent = get(ParentState, (parent, self) => {
  console.log('attached:', parent);
  return () => console.log('detached');
});

Callback runs when upstream is found/replaced. Return a cleanup function, or void.

get(Type, true) - downstream collection

tabs = get(Tab, true); // readonly Tab[]

Collects all instances of Type below in the context tree. Updates as children are added or removed. Subclasses match; superclasses do not.

get(Type, true, callback) - downstream with lifecycle

items = get(Item, true, (item, self) => {
  console.log('registered:', item);
  return () => console.log('removed');
});

Return false from the callback to prevent registration. Return a function for cleanup on removal.

get(Type, true, true) - downstream single (required)

form = get(FormState, true, true);

get(Type, true, false) - downstream single (optional)

form = get(FormState, true, false); // T | undefined

Behavior notes

  • All get() properties are non-enumerable - excluded from snapshots, Object.keys(), and ref(this).
  • Upstream lookups check the direct parent first, then walk the context hierarchy.
  • A state does not resolve itself.
  • Upstream callbacks are not reactive - they fire once per mount.

ref

Mutable reference holder - like React's useRef, declared on the class.

ref<T>() - basic ref

element = ref<HTMLDivElement>();
element.current; // HTMLDivElement | null
element.current = div;

Returns a ref.Object<T> - simultaneously callable (like a React ref function) and a { current } object.

ref<T>(callback) - ref with callback

node = ref<HTMLElement>((el) => {
  console.log('attached:', el);
  return (next) => console.log('replaced with:', next);
});

Callback fires when the value is set (not on null by default). Returned function runs on replacement, receiving the new value.

ref<T>(callback, false) - callback includes null

node = ref<HTMLElement>((el) => {
  console.log('value is:', el); // fires for null too
}, false);

ref(this) - ref proxy

class Form extends State {
  name = '';
  email = '';
  refs = ref(this);
}

form.refs.name;          // ref.Object<string>
form.refs.name.current;  // current value of form.name
form.refs.name.current = 'new'; // updates form.name

Creates ref objects for every enumerable property. Must pass this - any other argument throws.

  • Reactive computed properties are included (read-only).
  • Factory-based properties (set(() => ...)) are excluded (non-enumerable).

ref(this, mapFn) - custom ref proxy

class Form extends State {
  name = '';
  inputs = ref(this, (key) => createInput(key));
}

Map function runs lazily once per key; return value is cached.

ref.Object<T> shape

interface ref.Object<T> {
  current: T;
  is: State;
  key: string;
  get(): T | null;
  get(callback: (value: T) => void): () => void;
}

def

Low-level primitive for building custom property behavior. All other instructions are built on def. Reach for it when you want reactive semantics that don't fit set, get, or ref.

custom = def((key, subject, state) => ({
  value: 'initial',
  enumerable: true,
  get: (source) => computedValue,
  set: (next, prev) => { /* ... */ },
  destroy: () => { /* cleanup */ },
}));

The factory runs during activation and can return:

  • void - side effect only, no property config.
  • () => void - cleanup function only.
  • A def.Config<T> object - full property configuration.

Config fields

FieldTypeDescription
valueTInitial value
enumerablebooleanAppear in Object.keys() (default true)
get(source: State) => T | true | falseCustom getter, or suspense/optional flag
set(next: T, prev: T) => void | T | falseCustom setter, or false for read-only
destroy() => voidCleanup on destruction

Setter callbacks behave like set() callbacks: throw false to reject, throw true to accept silently, return a value to transform.

On this page