Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/pocket-stack/pocketjs/llms.txt

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

@pocketjs/framework/lifecycle exposes component-scoped hooks into the PocketJS frame loop. Each hook registers a callback that is automatically cleaned up when the owning Solid owner (or Vue scope) disposes — onCleanup for Solid, onScopeDispose for Vue Vapor. Call these functions inside a component body or a reactive effect; calling them outside a reactive context will leave stale callbacks registered forever. The frame loop contract: once per vblank/rAF tick the host calls globalThis.frame(buttons). The runtime runs frame hooks first (user callbacks via onFrame/onButtonPress), then input edge-detection and focus navigation, then the renderer’s end-of-frame sweep that destroys detached subtrees. This ordering means reactive state updates from button presses are applied before the sweep, so remove-then-reinsert within a single frame always works correctly.
import {
  onFrame,
  onButtonPress,
  createSpriteAnimation,
  pushButtonHandlerBlock,
} from '@pocketjs/framework/lifecycle';
import type { ButtonPressOptions, SpriteAnimationOptions } from '@pocketjs/framework/lifecycle';

onFrame

Registers a callback to run once per vblank tick. The callback receives the current raw BTN bitmask as its argument. Use onFrame for frame counters, per-frame animation drivers, HUD updates, or any logic that must execute on every tick.
function onFrame(callback: (buttons: number) => void): void
callback
(buttons: number) => void
required
Called once per host frame. buttons is the full held-state bitmask using the BTN constants from @pocketjs/framework/input. The callback runs before the input edge-detection stage, so buttons reflects the raw held state for this tick — not a pressed edge.
The callback is removed automatically when the owning component or effect is disposed. There is no return value — cleanup is handled by onCleanup internally. Example — frame counter and per-frame HUD update:
import { createSignal } from 'solid-js';
import { onFrame } from '@pocketjs/framework/lifecycle';
import { BTN } from '@pocketjs/framework/input';

function HUDTimer() {
  const [frames, setFrames] = createSignal(0);

  onFrame(() => {
    setFrames(f => f + 1);
  });

  return <Text class="text-sm text-white">{() => `Frame ${frames()}`}</Text>;
}

onButtonPress

Registers an edge-triggered button handler. Fires once when the specified button transitions from up to down — not continuously while held. The callback is removed automatically when the component or effect is disposed.
function onButtonPress(
  mask: number,
  callback: (pressed: number, buttons: number) => void,
  opts?: ButtonPressOptions,
): void
mask
number
required
The BTN constant or bitmask to listen for. Combine multiple buttons with bitwise OR to fire when any of them is pressed (e.g. BTN.CROSS | BTN.TRIANGLE).
callback
(pressed: number, buttons: number) => void
required
Called on button-down. pressed is the just-pressed bitmask subset of mask; buttons is the full held bitmask for this frame. Use buttons to detect modifier combos.
opts
ButtonPressOptions
Optional gate and block-override options. See ButtonPressOptions below.
Example — back navigation with CROSS:
import { onButtonPress } from '@pocketjs/framework/lifecycle';
import { BTN } from '@pocketjs/framework/input';

function SettingsScreen(props: { onBack: () => void }) {
  onButtonPress(BTN.CROSS, () => {
    props.onBack();
  });

  return <Screen></Screen>;
}
Example — combo check with two buttons:
onButtonPress(BTN.LTRIGGER | BTN.RTRIGGER, (pressed, buttons) => {
  // fires when EITHER trigger is pressed; check `buttons` for the other held
  if ((buttons & BTN.LTRIGGER) && (buttons & BTN.RTRIGGER)) {
    openDebugOverlay();
  }
});

ButtonPressOptions

Options that control when an onButtonPress handler fires.
FieldTypeDefaultDescription
allowWhenBlockedbooleanfalseWhen true, the handler fires even while a modal or system block is active (i.e. while pushButtonHandlerBlock depth is greater than zero). Normal app handlers should leave this false.
activeboolean | (() => boolean)trueGate the handler. When false (or when the getter returns false), the callback is suppressed for that frame. Accepts a reactive getter — changes take effect on the next frame.

pushButtonHandlerBlock

Pushes a global block that suppresses all onButtonPress handlers that do not opt in with allowWhenBlocked: true. Returns an unblock function; call it when the blocking condition ends. Modal uses this internally to capture input while open.
function pushButtonHandlerBlock(): () => void
Blocks can be nested — each call increments an internal depth counter, and each returned disposer decrements it. Handlers are only unblocked when the counter reaches zero. Returns: () => void — call this function to unblock. Calling it more than once is safe (subsequent calls are no-ops). Example — custom blocking overlay:
import { pushButtonHandlerBlock } from '@pocketjs/framework/lifecycle';
import { onMount, onCleanup } from 'solid-js';

function BlockingOverlay(props: { open: () => boolean }) {
  createEffect(() => {
    if (!props.open()) return;
    const unblock = pushButtonHandlerBlock();
    onCleanup(unblock);
  });

  return <Show when={props.open()}></Show>;
}

createSpriteAnimation

Cycles through a list of image src keys on each host frame, returning a reactive accessor for the current frame key. Use this when you need a JS-driven frame sequence rather than a baked Sprite atlas — for example, mixing frames from different atlas sources or pausing at a specific frame reactively.
function createSpriteAnimation(
  frames: readonly string[],
  opts?: SpriteAnimationOptions,
): Accessor<string>
frames
readonly string[]
required
Ordered list of image src keys (bare names from the pak, matching what you pass to <Image src="…">). Must not be empty — throws if frames.length === 0.
opts
SpriteAnimationOptions
Optional timing configuration.
Returns: Accessor<string> — a reactive getter for the current frame key. Pass it to <Image src={currentFrame()} />.

SpriteAnimationOptions

FieldTypeDefaultDescription
frameStepnumber1Number of host frames each sprite frame remains visible. Minimum 1. A value of 2 halves the animation speed (30 fps at 60 Hz); 4 quarters it (15 fps).
Example — JS-driven two-frame blink:
import { createSpriteAnimation } from '@pocketjs/framework/lifecycle';

function BlinkingIcon() {
  const frame = createSpriteAnimation(['icon-open', 'icon-closed'], { frameStep: 15 });

  return <Image src={frame()} class="w-8 h-8" />;
}
For most sprite animations, prefer the native <Sprite> primitive — it plays directly from a baked atlas with zero per-frame JS. Use createSpriteAnimation only when you need reactive control over which frame is displayed or when blending frames from multiple pak entries.

Component lifecycle example

A complete example combining onFrame, onButtonPress, and a cleanup-safe pattern:
import { createSignal } from 'solid-js';
import { onFrame, onButtonPress } from '@pocketjs/framework/lifecycle';
import { BTN } from '@pocketjs/framework/input';
import { animate } from '@pocketjs/framework/animation';
import type { NodeMirror } from '@pocketjs/framework';

function GameHUD(props: { onPause: () => void }) {
  const [score, setScore] = createSignal(0);
  let scoreBar: NodeMirror | undefined;

  // Increment score each frame for demonstration
  onFrame(() => {
    setScore(s => s + 1);
  });

  // Press START to pause
  onButtonPress(BTN.START, () => {
    props.onPause();
  });

  // Flash the score bar when score crosses a threshold
  createEffect(() => {
    if (score() % 600 === 0 && scoreBar) {
      animate(scoreBar, 'scale', 1.2, { dur: 80, easing: 'out-back' });
      animate(scoreBar, 'scale', 1.0, { dur: 80, easing: 'out', delay: 80 });
    }
  });

  return (
    <View class="absolute top-2 left-2 right-2 flex-row items-center justify-between">
      <View ref={(n) => { scoreBar = n; }} class="px-2 py-1 bg-white rounded">
        <Text class="text-sm font-bold">{() => `Score: ${score()}`}</Text>
      </View>
    </View>
  );
}
Both onFrame and onButtonPress are cleaned up automatically when GameHUD unmounts — no manual teardown is required.

Build docs developers (and LLMs) love