PocketJS uses the selected framework’s reactive system directly. Solid apps import signals and lifecycle fromDocumentation 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.
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.
- Solid
- Vue Vapor
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 mutations —
setText,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.
- Solid
- Vue Vapor
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.- Solid
- Vue Vapor
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.- Solid
- Vue Vapor
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:
- Solid
- Vue Vapor
- Solid
- Vue Vapor
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:
For in Practice
The notifications demo uses <For> to render a list that actually shrinks (items are dismissed):
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:
untrack reads a signal without subscribing to it — the current effect or memo will not re-run when that signal later changes:
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.
QuickJS Constraints on PSP
The PSP runs JavaScript on QuickJS (Bellard’s engine, roughly ES2023). That host has no scheduler: nosetTimeout, 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 import | Why |
|---|---|
createResource | Needs async scheduling; QuickJS has no task queue or timers. |
useTransition | Time-slicing needs a scheduler (setTimeout / MessageChannel). |
startTransition | Same — 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/animationor a Tailwindtransition-*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.
See Also
- Components —
View,Text,Image, and Solid control flow on the native tree - Animation —
animate(),spring(), and declarative transitions - Input & Focus —
onFrame,onButtonPress, and focus management