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
| Form | Enumerable | Writable | Notes |
|---|---|---|---|
= "foo" (plain) | yes | yes | Normal data property |
= set("foo") | no | yes | Managed default, excluded from snapshots |
= set("foo", cb) | no | yes | Managed with setter callback |
= set(() => "foo") | no | no | Factory, read-only |
= set(() => "foo", cb) | no | yes | Factory made writable by callback |
= set((from) => ...) | yes | no | Reactive computed, included in snapshots |
= set<T>() | no | yes | Placeholder, suspends on access |
= set(promise) | no | no | Direct 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 accessZero-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.
fromis a tracking proxy - reads create subscriptions.this(inside the function) does not create subscriptions - usefromfor tracked reads.- Chained computed values evaluate in declaration order.
- Can reference its own previous value via
from.ownPropwithout 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 | undefinedget(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 | undefinedBehavior notes
- All
get()properties are non-enumerable - excluded from snapshots,Object.keys(), andref(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.nameCreates 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
| Field | Type | Description |
|---|---|---|
value | T | Initial value |
enumerable | boolean | Appear in Object.keys() (default true) |
get | (source: State) => T | true | false | Custom getter, or suspense/optional flag |
set | (next: T, prev: T) => void | T | false | Custom setter, or false for read-only |
destroy | () => void | Cleanup on destruction |
Setter callbacks behave like set() callbacks: throw false to reject, throw true to accept silently, return a value to transform.