Create an isolated Tilia context with its own reactive world.
functionmake(gc?:number): Tilia
let make:(~gc:int=?, unit)=> tilia
make creates a context object containing the Tilia API (tilia, carve, observe, watch, batch, signal, derived, source, store, _observe).
Each context is isolated: observers and proxies from one context do not share tracking with another context. Use this for uncorrelated reactive worlds.
gc sets the cleared-watcher garbage-collection threshold. Default is 50. See tilia, Tilia, and guide chapter Mistakes stay small.
Example
import{ make }from"tilia";const a =make();const b =make();const left = a.tilia({ count:0});const right = b.tilia({ count:0});
left.count =1;
right.count =2;
openTilialet a =make()let b =make()let left = a.tilia({count:0})let right = b.tilia({count:0})
left.count=1
right.count=2
tilia converts a plain object or array into a proxy that tracks property reads and writes. The return value keeps the same shape and type as the input.
Nested plain objects and arrays are proxied lazily when read. Values with non-plain prototypes (for example class instances) are returned as-is. Calling tilia on a value that is not an object or array throws. Calling tilia on an already proxied value returns the same proxy.
Writing the same value (or the same underlying target object) does not notify observers. See also observe, computed, and guide chapter A Living Object.
Example
import{ tilia }from"tilia";const alice =tilia({
name:"Alice",
age:10,});
alice.age =11;
openTilialet alice =tilia({
name:"Alice",
age:10,})
alice.age=11
Build a reactive object where fields can derive from the full object.
functioncarve<T>(fn:(deriver: Deriver<T>)=>T):T
let carve:(deriver<'a>=>'a)=>'a
carve builds a proxied object from a factory function and provides a deriver argument with derived(self => ...).
derived is evaluated with the carved object as input. This allows cross-field derivation and methods that depend on sibling fields. The returned object is then proxied like tilia.
Use carve when derivation needs self; use computed when a standalone closure is enough. See guide chapter Carving a feature.
Run a tracked callback immediately and on dependency changes.
functionobserve(fn:()=>void):()=>void
let observe:(unit => unit)=> unit => unit
observe runs fn once immediately, tracks reactive reads during that run, and re-runs fn whenever one of those tracked keys changes. Dependency tracking is rebuilt on each run.
Writes performed inside fn are deferred while fn is running. If fn writes to keys it also tracks, it is scheduled to run again after the current run finishes. This makes observe suitable for state-machine style transitions.
observe returns a function that cancels the observation: once called, the callback never runs again. Ignore it for observers that should live as long as the context. For two-phase capture/effect behavior, use watch. For pull reactivity, use computed. See guide chapters A living object and While Alice sleeps.
watch separates reactive work into two phases: fn captures dependencies and returns a value; effect receives that value when captured dependencies change.
A watch never re-triggers itself from its own writes, in capture or effect; the effect runs untracked, and its writes notify other observers deferred, as one batch. On initial registration, fn runs once to install dependencies and effect is not called. watch returns a function that stops the watch: once called, neither phase runs again.
openTilialet(score, setScore)=signal(0)let(result, setResult)=signal("pending")watch(()=> result.value,
value =>{if value ==="pass"{setScore(score.value+1)}},)setResult("pass")
Group multiple writes and flush notifications once.
functionbatch(fn:()=>void):void
let batch:(unit => unit)=> unit
batch runs fn with notification flushing locked, then flushes once when the outermost batch ends.
Nested batches are supported. While inside a batch, writes update state immediately but observers are notified only after unlock. This prevents transient intermediate notifications.
observe, watch effects, and computed rebuilds already run under deferred flushing. Use batch for grouped writes from non-reactive callbacks. See watch and guide chapter While Alice sleeps.
Define a pull-based cached value for insertion into reactive objects.
functioncomputed<T>(fn:()=>T):T
let computed:(unit =>'a)=>'a
computed creates a dynamic value intended to be assigned directly in a tilia/carve object. The value is computed on read, cached, and invalidated when tracked dependencies change.
If no observer depends on the computed key, Tilia can clear its internal observer and keep the dynamic definition for later reads. Replacing or deleting the property removes the previous computed behavior.
Using a computed value outside insertion context raises an orphan-computation error. Define it directly where it is inserted. See tilia, carve, and guide chapter Values that follow.
source creates a dynamic value for insertion in a reactive object. It starts from initialValue, then executes fn(previous, set) on first read and whenever tracked dependencies in fn change.
previous is the latest value held by the source, and set updates it. If fn does asynchronous work, dependencies must still be read synchronously before awaiting; only synchronous reads are tracked.
If dependencies change, the current value stays available until set is called again — below, the previous cards stay visible while the new deck loads. See store, carve, and guide chapter Letting the world in.
When an empty result is valid, use a stable empty tilia value to distinguish the initial loading state without changing the value's type. Compare by identity:
Example
const loading =tilia<Card[]>([]);const app =tilia({
cards:source(loading,(_previous, set)=>{fetchCards("spanish").then(set);}),});if(app.cards === loading){// No result has arrived yet.}
let loading =tilia([])let app =tilia({
cards:source(loading,(_previous, set)=>{fetchCards("spanish")->Promise.thenResolve(set)->ignore
}),})if app.cards=== loading {// No result has arrived yet.}
The sentinel marks only the initial load. On a dependency change, source keeps its previous value until the next call to set; call set(loading) synchronously if a reload should return to the loading state.
Define an inserted managed value from a setup function with setter.
functionstore<T>(fn:(set: Setter<T>)=>T):T
let store:(('a => unit)=>'a)=>'a
store creates a dynamic inserted value from fn(set). The setup returns the current value and receives set to update it later.
The setup runs on first access and can re-run when tracked dependencies used during setup change. This is suitable for finite-state values where transitions call set.
openTiliatypesession=Idle({start: unit => unit})|Running({stop: unit => unit})letrec idle = set =>Idle({start:()=>set(running(set))})and running = set =>Running({stop:()=>set(idle(set))})let app =tilia({session:store(idle)})
Create a single-value reactive signal and its setter.
functionsignal<T>(value:T):[Signal<T>, Setter<T>]
let signal:'a =>(signal<'a>, setter<'a>)
signal returns a pair: a reactive object { value } and a setter function.
The signal object is a Tilia proxy, so reads of .value are tracked and writes through the setter notify dependents. It is a compact form for single mutable values.
Use derived to build a computed signal and lift to expose a signal in a tilia object.
Example
import{ signal }from"tilia";const[count, setCount]=signal(0);setCount(1);
count.value;
Lift a signal into a computed value for object insertion.
functionlift<T>(s: Signal<T>):T
let lift: signal<'a>=>'a
lift converts a signal into an inserted computed value by tracking s.value.
It is equivalent to computed(() => s.value), and is used when an object should expose a signal as a read-only field while keeping mutation through the signal setter.
Opaque observer handle used by low-level observer lifecycle helpers.
typeObserver={readonly[o]:true}
typeobserver
Observer/observer is an opaque handle representing a registered observer.
Application code using public APIs normally does not create or consume this type directly. It appears in low-level lifecycle helpers (_observe, _done, _ready, _clear).
For ordinary reactive effects, use observe or watch.
Wrap a React component with exact render dependency tracking.
functionleaf<T,U>(fn:(p:T)=>U):(p:T)=>U
let leaf:('a =>'b)=>'a =>'b
leaf wraps a component so reads of Tilia proxies during render are tracked with exact render boundaries.
When tracked keys change, the wrapped component re-renders. The API is equivalent to a higher-order component and is the preferred React integration over useTilia.
useTilia enables reactive tracking for the current component render. Call it at the top of the component.
Reads of Tilia proxies during render become dependencies. When one of those dependencies changes, the component re-renders. useTilia is the hook form; leaf is the preferred wrapper when possible.
Compute a React value and re-render only when the result changes.
functionuseComputed<T>(fn:()=>T):T
let useComputed:(unit =>'a)=>'a
useComputed evaluates fn in reactive tracking and returns its result.
The hook compares computed results and re-renders when the result value changes. This differs from plain render reads, which depend on every tracked key read during render.
Create a React API bound to a specific Tilia context.
functionmake(tilia: Tilia): TiliaReact
let make: tilia => tilia_react
@tilia/reactmake builds a React integration object (useTilia, useComputed, leaf) from a provided core Tilia context.
Use this with core make when an application needs isolated, uncorrelated reactive worlds.
The package-level exports are the default-context version; this function is the context-bound version.
Example
import{ make as makeCore }from"tilia";import{ make as makeReact }from"@tilia/react";const ctx =makeCore();const reactApi =makeReact(ctx);void reactApi.useTilia;
let ctx =Tilia.make()let reactApi =TiliaReact.make(ctx)ignore(reactApi.useTilia)
import{ make as makeCore }from"tilia";import{ make as makeReact }from"@tilia/react";importtype{ TiliaReact }from"@tilia/react";const reactApi: TiliaReact =makeReact(makeCore());void reactApi.leaf;