Documentation

Complete guide to using Tilia for simple and fast state management in TypeScript and ReScript applications.

Installation

Use the canary version (not yet released because the API might still change, but it is stable):

npm install tilia@canary

Goals and Non-goals

The goal with Tilia is to be minimal and fast while staying out of the way. A special effort was made to keep the API simple and intuitive, while supporting best practices (type safety, proper management of transitive states, etc).

We haven't measured the performance of the library yet, but everything was designed to make it as fast and lightweight as possible. If someone wants to help us benchmarking, we'd be happy to add this information to the documentation.

Non-goal Tilia is not a framework.

API Reference

connect

Connect an object or array to the forest so that it can be observed.

We use the default context. If we want to have multiple contexts, we can create them with make which will return a new isolated set of functions (connect, observe, etc.).

import { connect } from "tilia"

const alice = connect({
  name: "Alice",
  birthday: dayjs("2015-05-24"),
  age: 10,
})

Alice can now be observed. Who knows what she will be doing?

observe

Observe and react to changes. Every time the callback is run, tilia registers which values are read in the connected objects and arrays and will notify the observer if any of these values changed.

import { observe } from "tilia"

observe(() => {
  console.log("Alice is now", alice.age, "years old !!")
})

📖 Important Note: If you have mutations of observed values in the observe callback, the callback will be immediately re-run (to support state machines).

Now every time alice's age changes, the callback will be called.

signal

Use signal to represent a value that changes, such as a date, a number, a variant or even the app as it goes through its life cycle.

import { signal } from "tilia"

// Ending signals with '_' can be useful to avoid name collisions and to recognize them.
const [now_, setNow] = signal(dayjs())

setInterval(() => setNow(dayjs()), 1000 * 60)

// To read the value, we simply do
console.log(now_.value)

💡 Pro tip: The signal function is just syntax sugar on top of connect.

function signal<a>(value: a): Signal<a> {
  const s = connect({ value })
  const set = (v) => (s.value = v)
  return [s, set]
}

Our app knows the date ! Let's see how we can update Alice's age from this value.

computed

Compute a value from other connected objects.

import { computed } from "tilia"

const alice = connect({
  name: "Alice",
  birthday: dayjs("2015-05-24"),
  age: computed(() => now_.value.diff(alice.birthday, "year")),
})

📖 Note: The computed only recomputes or notifies when needed. Notification happens if there are observers, and if the computed value changes.

Nice, the age updates automatically, Alice can grow older. Let's wish her a happy birthday when this happens.

observe to update

You can also use observe to update a value from the current value and other connected objects.

import { observe, signal } from "tilia"

const [userAge_, setUserAge] = signal(alice.age)

observe(() => {
  if (alice.age !== userAge_.value) {
    setUserAge(alice.age)
    console.log("Alice is now", alice.age, "years old !!")
    console.log("🥳🩷 Happy Birthday Alice !! 🩷🥳")
  }
})

This pattern lets us create state machines that are very useful for initialisation or other complex states. See app.ts for an example.

💡 Pro tip: Mutating an observed value in observe will retrigger the callback.

We feel good with our app now. It only wishes a happy birthday when the age changes, it knows the age of Alice.

Maybe we can use this to manage access to a social media ?

derived

Derive a signal from other connected objects (other signals or values).

import { derived } from "tilia"

const socialMediaAllowed_ = derived(() => userAge_.value > 13)

💡 Pro tip: derived is syntactic sugar on top of computed.

function derived<a>(fn: () => a): Signal<a> {
  return connect({ value: computed(fn) })
}

useTilia (React Hook)

The useTilia hook registers the component to observe state changes, automatically triggering a re-render whenever any observed value updates.

This hooks makes the component's render function act like an observe callback.

Installation

npm install @tilia/react@canary
import { useTilia } from "@tilia/react"

function App() {
  useTilia()

  if (socialMediaAllowed_.value === true) {
    return <SocialMediaApp />
  } else {
    return <NormalApp />
  }
}

Main Features

✓ Zero dependencies
✓ Single proxy tracking
✓ Compatible with ReScript and TypeScript
✓ Inserted objects are not cloned
✓ Tracking follows moved or copied objects
✓ Respects readonly or classes
✓ Leaf-tracking (observe read values only)
✓ Forest mode: tracking across multiple instances

Examples

You can check the todo app for a working example using TypeScript.

Look at tilia tests for working examples using ReScript.

Changelog

2025-05-24 2.0.0 (canary version)

  • Moved core to "tilia" npm package.
  • Changed make signature to build tilia context.
  • Enable forest mode to observe across separated objects.
  • Add computed to compute values in branches.
  • Moved observe into tilia context.
  • Added signal, and derived for FRP style programming.
  • Simplify useTilia signature.

See the full changelog in the README for previous versions.