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 uses the selected framework’s reactive system directly. Solid apps import signals and lifecycle from solid-js; Vue Vapor apps import refs, computed values, watchers, and lifecycle from vue. There is no PocketJS reactivity wrapper — if you already know either framework, you know the API.
import {
  createSignal,
  createEffect,
  createMemo,
  onMount,
  onCleanup,
  batch,
  untrack,
} from "solid-js";

Why No Virtual DOM

React re-renders a component, builds a new virtual tree, and diffs it against the old one. PocketJS’s supported frameworks avoid that per-update component re-render path. Setup wires reactive reads directly to native mutations, and after that the work is limited to the effects or bindings whose dependencies actually changed. That property is what makes PocketJS viable on a 2005 handheld:
  • No diffing. There is no reconciliation walk per frame. A signal update touches only the nodes that depend on it.
  • One FFI crossing per frame. The renderer keeps a JS mirror tree of the native node tree, so Solid’s reconciler reads (parent, children, siblings) never cross into Rust. Only mutationssetText, setStyle, setProp, insertBefore — cross, and they are flushed as one batch per frame.
  • Effects only fire on interaction. In steady state (no signal changed) the JS side does essentially nothing. The Rust core still ticks animations and layout at a fixed 1/60 s. Idle screens cost no JS work.

Signals and Refs

A Solid signal is a getter/setter pair. A Vue ref is an object with a .value. Both represent one reactive value and trigger fine-grained updates on change.
import { View, Text } from "@pocketjs/framework/components";
import { createSignal } from "solid-js";

function Counter() {
  const [count, setCount] = createSignal(0);
  return (
    <View
      class="px-4 py-2 rounded-xl bg-blue-600 focus:bg-blue-500 transition-colors duration-150"
      focusable
      onPress={() => setCount(count() + 1)}
    >
      <Text class="text-base text-white font-bold">Count: {count()}</Text>
    </View>
  );
}

Signals in Text

Count: {count()} is not a special construct — it is a <Text> element with a static string and a dynamic expression. The renderer lays both out as a single concatenated inline run (one measure, not two flex items). When the reactive value changes the renderer calls replaceText on just the dynamic segment. The static prefix never re-measures unless it too changes. You can mix as many static and dynamic segments as you like inside one <Text> — they all fold into one inline run.

Effects and Watchers

An effect or watcher runs immediately, tracks every reactive value it reads, and re-runs whenever any of them changes. Use it for side effects — driving an animation, logging, imperative work — not for producing values you render.
import { createSignal, createEffect } from "solid-js";

const [level, setLevel] = createSignal(0);

createEffect(() => {
  // Re-runs every time level() changes.
  if (level() >= 100) console.log("charged");
});
Effects are the right place to bridge reactive state to imperative APIs like animate(): read a signal, and when it changes, kick a native tween. This is exactly how the settings demo’s toggle knob works — a createEffect reads the value signal and calls animate(knob, "translateX", x) on change.

Memo and Computed

A memo or computed value is a derived, cached reactive value. It re-computes only when one of its inputs changes.
import { createSignal, createMemo } from "solid-js";

const [items, setItems] = createSignal<string[]>([]);
const total = createMemo(() => items().length);

// total() is cached; it recomputes only when items() changes.

Mount and Cleanup

onMount / onMounted runs a callback once, after the component’s initial render — the right place for one-time imperative setup. The hero demo uses it to start the underline sweep-in:
import { View, type NodeMirror } from "@pocketjs/framework/components";
import { animate } from "@pocketjs/framework/animation";
import { onMount } from "solid-js";

function Underline() {
  let underline: NodeMirror | undefined;
  onMount(() => {
    // Runs once; the tween ticks natively — zero steady-state JS.
    if (underline) animate(underline, "width", 210, { dur: 700, easing: "out", delay: 150 });
  });
  return <View ref={underline} class="h-1 w-0 rounded-full bg-blue-500" />;
}
Cleanup callbacks run when the enclosing scope is disposed — a component unmounting, or an effect re-running. Use them to release anything you acquired imperatively:
import { createEffect, onCleanup } from "solid-js";
import { animate, cancelAnim } from "@pocketjs/framework/animation";

createEffect(() => {
  const anim = animate(node, "opacity", 1, { dur: 300 });
  onCleanup(() => cancelAnim(anim)); // runs before the next re-run / on dispose
});

Control Flow

In Solid apps, use Solid’s control-flow components for lists and conditionals. Vue Vapor apps use Vue’s native JSX patterns.

Show and Empty Text Nodes

<Show when={…}> renders a subtree conditionally. While when is false, Solid leaves behind an empty text marker. Empty text nodes are excluded from layout entirely — no width, no height, no gap in a gap-N row. They re-enter layout the moment they become non-empty. This is why toggling a <Show> inside a flex-row gap-2 row does not leave a phantom gap:
import { Show } from "solid-js";
import { Text } from "@pocketjs/framework/components";

// The gap-2 row only contains visible items — no phantom space when count <= 3
<View class="flex-row gap-2 items-center">
  <Text class="text-sm text-slate-600">Count: {count()}</Text>
  <Show when={count() > 3}>
    <Text class="text-sm text-emerald-600">Reactive on real hardware.</Text>
  </Show>
</View>

For in Practice

The notifications demo uses <For> to render a list that actually shrinks (items are dismissed):
import { For, onMount } from "solid-js";
import { View, Text, type NodeMirror } from "@pocketjs/framework/components";
import { animate } from "@pocketjs/framework/animation";

<For each={items()}>
  {(item, i) => {
    let el: NodeMirror | undefined;
    onMount(() => {
      if (el) {
        animate(el, "opacity", 1, { dur: 250, delay: i() * 70, easing: "out" });
        animate(el, "translateX", 0, { dur: 250, delay: i() * 70, easing: "out" });
      }
    });
    return (
      <View
        ref={el}
        style={{ opacity: 0, translateX: 16 }}
        class="flex-row items-center gap-3 p-1 rounded-lg shadow bg-white border-slate-200 focus:bg-blue-50 focus:border-blue-500 transition-colors duration-150"
        focusable
        onPress={() => dismiss(item.id, el)}
      >
        <View class={item.dotCls} />
        <View class="flex-col grow">
          <Text class="text-xs text-slate-950 font-bold">{item.title}</Text>
          <Text class="text-xs text-slate-600">{item.message}</Text>
        </View>
        <Text class="text-xs text-slate-500">{item.time}</Text>
      </View>
    );
  }}
</For>
Each item fades and slides in with a staggered delay driven by i(). When the array changes, For moves existing nodes to their new positions (the native insertBefore op) rather than destroying and recreating them. Focus and animation state survive a reorder.

batch and untrack (Solid)

batch groups multiple signal writes so dependent effects run once at the end, instead of after each write:
import { batch } from "solid-js";

batch(() => {
  setX(10);
  setY(20); // effects that read x() and y() run once, after the batch
});
untrack reads a signal without subscribing to it — the current effect or memo will not re-run when that signal later changes:
import { untrack } from "solid-js";

createEffect(() => {
  const live = trigger();            // tracked: re-runs when trigger() changes
  const snapshot = untrack(config);  // read once, not a dependency
  apply(live, snapshot);
});

The onFrame Callback

onFrame registers a callback that runs once per vblank tick, inside the frame handler, before the renderer sweep. It receives the current button bitmask and is cleaned up automatically when the owning component unmounts.
import { onFrame } from "@pocketjs/framework/lifecycle";
import { BTN } from "@pocketjs/framework/input";
import { createSignal } from "solid-js";

function Player() {
  const [x, setX] = createSignal(0);
  onFrame((buttons) => {
    if (buttons & BTN.LEFT)  setX((v) => v - 2);
    if (buttons & BTN.RIGHT) setX((v) => v + 2);
  });
  return <View style={{ translateX: x() }} class="w-8 h-8 rounded-lg bg-blue-600" />;
}

QuickJS Constraints on PSP

The PSP runs JavaScript on QuickJS (Bellard’s engine, roughly ES2023). That host has no scheduler: no setTimeout, no MessageChannel, and no performance. queueMicrotask is polyfilled via Promise.resolve().then(...), which is enough for Solid’s synchronous batching — but nothing that needs real timers can work. These Solid features are compile errors in PocketJS — the build’s Babel plugin lints their imports and fails the build rather than shipping something that breaks at runtime on hardware:
Banned importWhy
createResourceNeeds async scheduling; QuickJS has no task queue or timers.
useTransitionTime-slicing needs a scheduler (setTimeout / MessageChannel).
startTransitionSame — concurrent scheduling is unavailable on PSP.

What to Use Instead

  • For state that changes over time: signals + createEffect. Updates are synchronous and cheap — there is no “pending” transition state to model.
  • For motion: use animate() from @pocketjs/framework/animation or a Tailwind transition-* class. Both tick in Rust at a fixed 1/60 s; animation is a pure function of frame index and costs the JS side nothing per frame.
  • For “async” data: load it at build time into the app bundle / pak, or drive it from host input. There is no runtime fetch on the PSP.
Importing createResource, useTransition, or startTransition from solid-js is a compile error in PocketJS. The Babel plugin fails the build with a code frame pointing to the offending import rather than shipping code that silently fails at runtime.

See Also

  • ComponentsView, Text, Image, and Solid control flow on the native tree
  • Animationanimate(), spring(), and declarative transitions
  • Input & FocusonFrame, onButtonPress, and focus management

Build docs developers (and LLMs) love