Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/sanchedev/fraxel/llms.txt

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

Fraxel hooks are functions called inside component functions that provide reactivity, lifecycle management, and access to the scene graph. Like React hooks, they must be called at the top level of a component — but instead of targeting a Virtual DOM, they bind to canvas nodes and their events. Hooks are the primary way to create reactive state, respond to input, manage side effects, and communicate between components in a Fraxel game.

Native Hooks

These hooks are the core primitives provided by fraxel/hooks. All derived hooks are composed from these.
HookDescription
useNode(type)Creates a typed reference to pass as ref
useEvent(node, event, callback)Type-safe event subscription with auto-cleanup on destroy
useSignal(initial)Creates reactive state that triggers re-renders
useComputed(fn)Creates a derived signal that recomputes when deps change
useEffect(fn)Runs effect on mount and when signals change (batched)
useMount(fn)Runs once on mount, cleanup on destroy
useSpawn(node)Returns a function to dynamically spawn children
useGame()Access game controls: play, pause, changeScene
useChild(path, type)Gets a reference to a child node by path
useScript(ref)Retrieves the FraxelScript attached to a node
useTrigger(trigger, callback)Pub/sub for cross-component communication
createContext(default)Creates a context with a Provider component
useContext(context)Retrieves the current context value
useRef(value)Mutable reference that persists across renders

useNode

useNode(type) creates a NodeReference — a typed, reactive handle to a scene node. Pass it to a JSX element’s ref prop to bind the reference. Once the node mounts, nodeRef.node gives you imperative access to the underlying engine object.
import { useNode } from 'fraxel/hooks'
import { PrimaryNode } from 'fraxel'

function Player() {
  const sprite = useNode(PrimaryNode.Sprite)

  return (
    <transform>
      <sprite ref={sprite} textureId={PLAYER_TEXTURE} />
    </transform>
  )
}
The PrimaryNode enum lists every built-in node type:
import { PrimaryNode } from 'fraxel'

PrimaryNode.Group          // 'group'
PrimaryNode.Transform      // 'transform'
PrimaryNode.Sprite         // 'sprite'
PrimaryNode.AnimationPlayer // 'animation-player'
PrimaryNode.Collider       // 'collider'
PrimaryNode.RayCast        // 'ray-cast'
PrimaryNode.Clickable      // 'clickable'
PrimaryNode.Timer          // 'timer'
PrimaryNode.Rectangle      // 'rectangle'
PrimaryNode.Text           // 'text'
PrimaryNode.AudioPlayer    // 'audio-player'
PrimaryNode.Camera         // 'camera'
PrimaryNode.RigidBody      // 'rigid-body'
NodeReference also exposes a .signal getter — a SignalGetter<NodeInstances[T] | null> — so you can reactively track when the node becomes available.

useEvent

useEvent(node, event, callback) subscribes to a named event on a NodeReference. The subscription is automatically removed when the node is destroyed — no manual cleanup needed. The event names are fully type-safe based on the node type passed to useNode.
import { useNode, useEvent } from 'fraxel/hooks'
import { PrimaryNode, shapes } from 'fraxel'

function Enemy() {
  const collider = useNode(PrimaryNode.Collider)

  useEvent(collider, 'colliderEntered', (other) => {
    console.log('Hit by:', other)
  })

  return (
    <collider
      ref={collider}
      shape={shapes.circle(8)}
      group={['enemy']}
      collidesWith={['projectile']}
    />
  )
}
useEvent auto-cleans up the listener when the node is destroyed. You never need to call .off() manually for events registered this way.

useSignal

useSignal(initial) creates a reactive signal. It returns a [getter, setter] tuple. The getter is a callable function — you must call it as health() to read the value, not health directly. Calling the getter inside a useEffect or JSX prop registers it as a dependency.
import { useSignal, useEffect } from 'fraxel/hooks'

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

  useEffect(() => {
    console.log('Health changed:', health())
  })

  return (
    <rectangle
      size={[health(), 16]}
      fillColor={() => [1 - health() / 100, health() / 100, 0, 1]}
    />
  )
}
The setter accepts the new value directly:
setHealth(80)
setHealth(health() - 10) // reduce by 10
Signals clean up their subscribers automatically when the owning node is destroyed.

useComputed

useComputed(fn) creates a derived signal whose value is computed from other signals. It re-runs fn whenever any signal accessed inside it changes, and returns a SignalGetter for the result.
import { useSignal, useComputed } from 'fraxel/hooks'

function Cooldown() {
  const [time, setTime] = useSignal(0)
  const progress = useComputed(() => time() / 3) // 3-second cooldown

  return (
    <rectangle
      size={[128, 8]}
      fillColor={() => [1 - progress(), progress(), 0, 1]}
    />
  )
}
useComputed is purely derived — it has no setter. To update it, update the upstream signal(s) it depends on.

useEffect

useEffect(fn) runs an effect when the node starts, and re-runs it whenever any signal accessed inside fn changes. If fn returns a function, that cleanup is called before each re-run and on node destroy.
import { useSignal, useEffect } from 'fraxel/hooks'

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

  useEffect(() => {
    if (health() <= 0) {
      console.log('Enemy died')
    }
    return () => {
      console.log('Effect cleaned up')
    }
  })

  return <transform />
}

Batching

useEffect batches synchronous signal changes — if multiple signals are updated in the same synchronous block, the effect runs once, not once per signal. Re-executions are scheduled via queueMicrotask and run before the next frame.
useEffect(() => {
  console.log(x(), y(), z())
})

// All three trigger only a single effect execution
setX(1)
setY(2)
setZ(3)
Batching makes it safe to call multiple setters in a row (e.g., inside a physics update) without causing unnecessary re-renders. The canvas always sees a consistent state.

useMount

useMount(fn) runs a function once when the node’s start() lifecycle fires. It does not re-run on signal changes. If fn returns a cleanup function, that cleanup runs when the node is destroyed.
import { useNode, useMount } from 'fraxel/hooks'
import { PrimaryNode } from 'fraxel'

function GameScene() {
  const camera = useNode(PrimaryNode.Camera)
  const player = useNode(PrimaryNode.Sprite)

  useMount(() => {
    camera.node.follow(player.node)

    return () => {
      console.log('Scene destroyed')
    }
  })

  return (
    <camera ref={camera} zoom={2}>
      <sprite ref={player} textureId={PLAYER} />
    </camera>
  )
}
Use useMount for one-time setup such as registering subscriptions, starting timers, or calling imperative node methods that require the node to be started first.

useSpawn

useSpawn(node) returns a spawn function that renders a JSX expression and appends the resulting node as a child of the given NodeReference. This is the primary way to dynamically create game objects at runtime.
import { useNode, useSpawn } from 'fraxel/hooks'
import { PrimaryNode } from 'fraxel'

function Spawner() {
  const container = useNode(PrimaryNode.Transform)
  const spawn = useSpawn(container)

  return (
    <transform ref={container}>
      <clickable
        size={[64, 32]}
        onClick={() => spawn(<Enemy />)}
      />
    </transform>
  )
}
The spawned node receives the current context (including context providers) from the calling component, so context values propagate correctly to spawned children.

useGame

useGame() returns GameControls — an object with methods to control global game state: pause, resume, and change scenes.
import { useGame } from 'fraxel/hooks'

function PauseButton() {
  const game = useGame()

  return (
    <clickable
      size={[64, 32]}
      onClick={() => game.pause()}
    />
  )
}
GameControls API
MethodSignatureDescription
play()() => voidResume the game loop
pause()() => voidPause the game loop
changeScene(name)(name: string) => Promise<void>Transition to a named scene
preloadScene(name)(name: string) => Promise<void>Preload a scene without transitioning
getSize()() => Vector2Returns the canvas dimensions

createContext / useContext

Fraxel contexts allow you to pass values through the component tree without prop drilling. createContext(defaultValue) creates a context object with a Provider component. useContext(ctx) reads the nearest Provider value above the calling component.
import { createContext, useContext } from 'fraxel/hooks'

const GameCtx = createContext({ score: 0 })

function GameScene() {
  return (
    <GameCtx.Provider value={{ score: 42 }}>
      <ScoreLabel />
    </GameCtx.Provider>
  )
}

function ScoreLabel() {
  const { score } = useContext(GameCtx)

  return (
    <text
      text={`Score: ${score}`}
      position={[10, 10]}
      style={{ foregroundColor: '#fff', fontSize: 16 }}
    />
  )
}
If no matching Provider exists above the component, useContext returns the default value passed to createContext.

useTrigger / createTrigger

useTrigger provides a pub/sub pattern for cross-component communication that doesn’t use node events. Create a trigger with createTrigger<T>(), subscribe to it with useTrigger, and fire it with trigger.emit().
import { createTrigger, useTrigger } from 'fraxel/hooks'

// Shared trigger — declare outside components
const planted = createTrigger<[Plant]>()

// In a parent component — listen
function Garden() {
  useTrigger(planted, (plant) => {
    console.log('Planted:', plant)
  })

  return <transform><PlantPicker /></transform>
}

// In a child component — emit
function PlantPicker() {
  return (
    <clickable
      size={[64, 32]}
      onClick={() => planted.emit(Plant.Peashooter)}
    />
  )
}
useTrigger automatically disconnects the callback when the node is destroyed. Direct use of the Trigger class is also available:
MethodDescription
trigger.connect(fn)Subscribe a callback
trigger.disconnect(fn)Unsubscribe a callback
trigger.emit(...args)Fire all connected callbacks

useRef

useRef(value) creates a Reference object whose .current property persists across renders and signal updates. Unlike useSignal, mutating .current does not trigger any reactive updates.
import { useRef } from 'fraxel/hooks'

function ClickCounter() {
  const count = useRef(0)

  return (
    <clickable
      size={[64, 32]}
      onClick={() => {
        count.current++
        console.log('Clicks:', count.current)
      }}
    />
  )
}
Use useRef for values you need to track across renders (like frame counters, previous values, or imperative handles) that shouldn’t trigger reactive re-computation.

useChild

useChild(path, type) returns a NodeReference to a descendant node located at the given path of id strings. The reference is resolved once after the root node starts.
import { useChild } from 'fraxel/hooks'
import { PrimaryNode } from 'fraxel'

function Scene() {
  const healthBar = useChild(['hud', 'health-bar'], PrimaryNode.Rectangle)

  return (
    <transform>
      <transform id="hud">
        <rectangle id="health-bar" size={[100, 8]} fillColor={[0, 1, 0, 1]} />
      </transform>
    </transform>
  )
}
useChild requires the component to render a single root node. The path is an array of id strings navigating down the tree.

useScript

useScript(ref) returns a SignalGetter containing the FraxelScript instance attached to a given NodeReference. The getter returns undefined until the node and its script have started.
import { useNode, useScript } from 'fraxel/hooks'
import { PrimaryNode } from 'fraxel'

class PlayerScript extends FraxelScript<PrimaryNode.Sprite> {
  health = 100
}

function Player() {
  const sprite = useNode(PrimaryNode.Sprite)
  const script = useScript<PlayerScript>(sprite)

  useEffect(() => {
    if (script()) {
      console.log('Player health:', script()!.health)
    }
  })

  return <sprite ref={sprite} textureId={PLAYER} script={new PlayerScript()} />
}

Derived Hooks

Derived hooks are composed from native hooks to provide domain-specific abstractions. They simplify common patterns without introducing unique runtime behavior.
HookDescription
useCondition(node, on, off)Reactive boolean toggled by two opposing events on a node
useMatch(signal, record)Maps a signal value to a record (like a switch expression)
useWhen(signal, trueVal, falseVal)Ternary computed value based on a boolean signal
useClickable(ref?)Clickable node reference with reactive hovered and position state
useTimer(ref?)Timer node with reactive time, progress, and controls
useRayCast(ref?)RayCast node with reactive detected state and collider
useCollider(ref?)Collider node with reactive colliding state and other
useAnimation(ref?)AnimationPlayer with reactive frame state and control methods
useAudio(ref?)AudioPlayer with reactive playing state and controls

useCondition

Creates a reactive boolean that flips true when on event fires and false when off event fires on the given node:
import { useCondition, useNode } from 'fraxel/hooks'
import { PrimaryNode } from 'fraxel'

function EnemyDetector() {
  const ray = useNode(PrimaryNode.RayCast)
  const isDetected = useCondition(ray, 'colliderEntered', 'colliderExited')

  return (
    <ray-cast
      ref={ray}
      direction={[100, 0]}
      collidesWith={['enemy']}
      brightness={() => isDetected() ? 1.5 : 1}
    />
  )
}

useClickable

Returns a Clickable node reference, a reactive hovered boolean, and a reactive position signal tracking the pointer’s local coordinates:
import { useClickable, useComputed } from 'fraxel/hooks'

function Button() {
  const { ref, hovered, position } = useClickable()
  const brightness = useComputed(() => hovered() ? 1.1 : 1)

  return (
    <sprite ref={ref} textureId={BTN} brightness={brightness}>
      <clickable size={[64, 32]} onClick={handleClick} />
    </sprite>
  )
}
  • ref: NodeReference<PrimaryNode.Clickable> — the node reference
  • hovered: SignalGetter<boolean>true while the pointer is inside the area
  • position: SignalGetter<Vector2> — current pointer position in local space, updated every frame while hovering

useTimer

Returns a Timer node reference plus reactive time and progress signals, and control methods:
import { useTimer } from 'fraxel/hooks'

function Cooldown() {
  const { ref, time, progress, play, pause, stop } = useTimer()

  return (
    <transform>
      <timer ref={ref} duration={3} autoPlay />
      <rectangle
        size={() => [progress() * 100, 10]}
        fillColor={[0, 1, 0, 1]}
      />
    </transform>
  )
}
  • time: SignalGetter<number> — current elapsed seconds
  • progress: SignalGetter<number>0 to 1 based on duration
  • play(), pause(), stop() — control methods

useRayCast

Returns a RayCast node reference with detected and collider reactive signals:
import { useRayCast, useComputed } from 'fraxel/hooks'

function EnemyDetector() {
  const { ref, detected, collider } = useRayCast()
  const name = useComputed(() => collider()?.node.name ?? 'none')

  return <ray-cast ref={ref} direction={[100, 0]} collidesWith={['enemy']} />
}
  • detected: SignalGetter<boolean>true while a collider is hit
  • collider: SignalGetter<Collider | null> — the currently detected collider

useCollider

Returns a Collider node reference with colliding and other reactive signals:
import { useCollider } from 'fraxel/hooks'
import { shapes } from 'fraxel'

function Player() {
  const { ref, colliding, other } = useCollider()

  return (
    <collider
      ref={ref}
      shape={shapes.circle(16)}
      group={['player']}
      collidesWith={['enemy']}
    />
  )
}
  • colliding: SignalGetter<boolean>true while overlapping another collider
  • other: SignalGetter<Collider | null> — the other collider in the pair

useAnimation

Returns an AnimationPlayer node reference with reactive animation state and control methods:
import { useAnimation } from 'fraxel/hooks'

function Enemy() {
  const { ref, animName, frameIndex, ended, play, setNext } = useAnimation()

  return (
    <animation-player
      ref={ref}
      animations={() => ({ idle: idleFrames, attack: attackFrames })}
      currentAnim="idle"
    />
  )
}
  • animName: SignalGetter<string> — current animation name
  • frameIndex: SignalGetter<number> — current frame index
  • ended: SignalGetter<boolean>true when the animation finishes
  • play(animName?), setNext(animName?) — control methods

useAudio

Returns an AudioPlayer node reference with playing state and control methods:
import { useAudio, useClickable } from 'fraxel/hooks'

function SoundEffect() {
  const { ref, playing, play, pause, stop } = useAudio()
  const { ref: btn } = useClickable()

  return (
    <sprite ref={btn}>
      <clickable size={[32, 32]} onClick={() => playing() ? pause() : play()} />
      <audio-player ref={ref} soundId={SOUND} />
    </sprite>
  )
}
  • playing: SignalGetter<boolean>true while audio is playing
  • play(), pause(), stop() — control methods

useMatch

Creates a computed value by mapping a signal’s current value to a record, like a switch expression:
import { useSignal, useMatch } from 'fraxel/hooks'

function Character() {
  const [state, setState] = useSignal<'idle' | 'walk' | 'jump'>('idle')

  const animation = useMatch(state, {
    idle: 'idle-anim',
    walk: 'walk-anim',
    jump: 'jump-anim',
  })

  return <animation-player currentAnim={animation} />
}

useWhen

Creates a computed value that switches between two values based on a boolean signal:
import { useCondition, useWhen, useNode } from 'fraxel/hooks'
import { PrimaryNode } from 'fraxel'

function HoverButton() {
  const clickable = useNode(PrimaryNode.Clickable)
  const isHovered = useCondition(clickable, 'mouseEntered', 'mouseExited')
  const brightness = useWhen(isHovered, 1.2, 1.0)

  return (
    <sprite brightness={brightness} textureId={BTN}>
      <clickable ref={clickable} size={[64, 32]} />
    </sprite>
  )
}

The List Component

List renders a reactive array of nodes with keyed reconciliation. When the array signal changes, only nodes whose keys are no longer present are destroyed; new keys create fresh nodes; existing keys are untouched.
import { List } from 'fraxel/jsx'
import { useSignal } from 'fraxel/hooks'

function EnemyList() {
  const [enemies, setEnemies] = useSignal([
    { id: 1, x: 0 },
    { id: 2, x: 50 },
  ])

  return (
    <List
      array={enemies}
      itemKey={(e) => e.id}
      empty={<text text="No enemies" position={[0, 0]} style={{ foregroundColor: '#aaa' }} />}
    >
      {(enemy) => (
        <transform position={[enemy.x, 0]}>
          <sprite textureId={ENEMY} />
        </transform>
      )}
    </List>
  )
}
Props
PropTypeDescription
arrayT[] | SignalGetter<T[]>Required. Reactive array to render
itemKey(value: T, index: number, arr: T[]) => string | symbolRequired. Unique key extractor
emptyFraxel.NodeFallback node when the array is empty
children(value: T, index: number, arr: T[]) => Fraxel.NodeRequired. Render function for each item
Each call to children must return exactly one root node. Use <transform> or <group> to wrap multiple nodes. List itself renders no wrapper — it uses an internal hidden <transform> as an anchor.

Nodes

Every JSX node, its props, events, and methods

Reactivity

Signals, computed values, and reactive prop bindings

Scripts

FraxelScript: attach reusable behavior to nodes

API: Hooks Core

Full TypeScript signatures for all native hooks

Build docs developers (and LLMs) love