Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/sanchedev/tiny-engine/llms.txt

Use this file to discover all available pages before exploring further.

tiny-engine’s reactivity system is built on signals — fine-grained reactive atoms that hold a single value and notify subscribers the moment that value changes. Rather than re-running component functions, the engine wires signal subscriptions directly to canvas property updates. This means only the exact canvas operations that depend on a changed signal are re-executed, keeping every frame as lean as possible.

useSignal — Reactive State

useSignal creates a signal and returns a [getter, setter] tuple. Call the getter (a zero-argument function) to read the current value; call the setter with a new value to update it and notify all dependants.
import { useSignal } from 'tiny-engine/hooks'

function Player() {
  const [health, setHealth] = useSignal(100)

  // Read inside a JSX prop — creates a reactive subscription automatically
  return (
    <transform>
      <sprite
        textureId={PLAYER}
        grayscale={() => (health() <= 0 ? 1 : 0)}
      />
    </transform>
  )
}
The [getter, setter] signature matches SignalGetter<T> and SignalSetter<T>:
export interface SignalGetter<T> { (): T }
export interface SignalSetter<T> { (value: T): void }

Auto-computed JSX Props

Any JSX prop that accepts a SignalGetter can receive a plain function () => value(). The engine calls that function once to subscribe to every signal read inside it. Whenever any of those signals change, only that one prop is updated on the canvas — no tree diff needed.
import { useSignal } from 'tiny-engine/hooks'

function HealthSprite() {
  const [health, setHealth] = useSignal(100)

  return (
    <sprite
      textureId={PLAYER}
      {/* Goes fully transparent (grayscale) when dead */}
      grayscale={() => (health() <= 0 ? 1 : 0)}
      {/* Dims as health drops from 200 → 0 */}
      brightness={() => 0.5 + health() / 200}
    />
  )
}
When setHealth(0) is called, grayscale flips to 1 and brightness drops to 0.5 in the same frame — no intermediate render pass.

useComputed — Derived Signals

useComputed derives a new signal from one or more existing signals. The computation function is re-evaluated automatically whenever a dependency changes.
import { useSignal, useComputed } from 'tiny-engine/hooks'

function Cooldown() {
  const [elapsed, setElapsed] = useSignal(0)

  // progress is a SignalGetter<number> that stays in [0, 1]
  const progress = useComputed(() => Math.min(elapsed() / 3, 1))

  return (
    <sprite
      textureId={COOLDOWN_BAR}
      brightness={() => 0.4 + progress() * 0.6}
    />
  )
}

refreshOnNodeStart

Pass true as the second argument to force the computed value to re-evaluate when the host node emits its started event. This is useful when the computation depends on a value (such as a script reference) that is only available after node initialisation:
const health = useComputed(
  () => scriptRef.current?.health.value ?? 4000,
  true, // refresh once the node has started
)

useEffect — Reactive Side Effects

useEffect runs a function when the host node starts and re-runs it whenever any signal read inside that function changes. Return a cleanup function to tear down subscriptions or timers when the effect re-runs or the node is destroyed.
import { useSignal, useEffect } from 'tiny-engine/hooks'

function Logger() {
  const [score, setScore] = useSignal(0)

  useEffect(() => {
    console.log('Score is now:', score())

    // Optional cleanup — called before the next run or on destroy
    return () => {
      console.log('Cleaning up score effect')
    }
  })

  return <transform />
}
The cleanup pattern is identical to the one used in frameworks like SolidJS — return a function from the effect to undo any work done in that run.

Signal — The Raw Primitive

useSignal is a hook wrapper around the lower-level Signal<T> class. You can use Signal directly when you need a reactive value outside a JSX component context, such as inside a TinyScript or a plain module.
import { Signal } from 'tiny-engine'

const health = new Signal(100)

// Subscribe manually
health.sub((value) => {
  console.log('Health changed to:', value)
})

// Update — notifies all subscribers
health.value = 50  // logs: "Health changed to: 50"

// Read current value
console.log(health.value) // 50

// Remove a specific subscriber
const onHealthChange = (v: number) => console.log(v)
health.sub(onHealthChange)
health.unsub(onHealthChange)

// Remove all subscribers at once
health.clearSubs()

Signal API Summary

MemberDescription
new Signal(initialValue)Creates a signal with the given initial value
.value (get)Returns the current value; registers a dependency if inside a tracked context
.value (set)Updates the value and notifies all subscribers (no-op if value is identical)
.sub(fn)Adds a subscriber callback
.unsub(fn)Removes a previously added subscriber callback
.clearSubs()Removes all subscriber callbacks
tiny-engine has no Virtual DOM. When a signal changes, the engine does not re-render a component tree — it directly updates the specific canvas property that subscribed to that signal. This makes reactivity extremely cheap: a single health.value = 50 call writes only to the canvas calls that depend on health, nothing else.

Build docs developers (and LLMs) love