Use when working with rEFui (refui) applications where you cannot rely on reading the library source. Covers the retained-mode + signals mental model, DOM/HTML/Reflow renderers, JSX setup (classic pragma vs automatic runtime), directives (on:/class:/style:/attr:/prop:/m: macros), HMR via refurbish/refui/hmr, debugging “UI not updating” issues, and migrating React/Vue/Solid/Svelte patterns to rEFui.
Apply rEFui’s retained-mode + signals model correctly, choose the right JSX mode and renderer, and fix reactivity or lifecycle issues without importing assumptions from other UI frameworks. This file is the operative guide: do not require repository documentation, a linked local checkout, MCP documentation, or the bundled reference files to complete ordinary rEFui work.
{signal.value} in JSX is static. Use {signal} or a derived $(() => ...).sig.trigger() (or replace with a new value).<If condition={sig}> and <If condition={computed}> are intended usage. The pitfall is conditional .value reads inside JS/derived code that skip later dependencies..value too early (non-reactively) or mutated in place without trigger().const count = signal(0){count} (not {count.value})const label = $(() => Count: ${count.value}) and then {label}sig.trigger() after mutating arrays/objects.import { signal, $ } from 'refui'
const Counter = () => {
const count = signal(0)
return (
<button on:click={() => count.set(count.value + 1)}>
{$(() => `Count: ${count.value}`)}
</button>
)
}
watch(() => { ...reads signals... })useEffect(() => { ...; return () => cleanup })onDispose(() => cleanup)EffectScope is the ownership basis for components, effects, and disposer scopes. Work created inside a live scope is automatically released with that owner.connect(signals, callback) and signal.connect(callback) subscribe only to the explicitly supplied signals. Reads inside their callbacks are intentionally untracked; create watch() explicitly when a callback needs discovered dependencies.useAction listeners are event handlers, not reactive effects. Signal reads and writes inside a listener do not subscribe the listener; listener registration is still disposed with its outer scope.await nextTick().<If condition={cond}>{() => <Then />}{() => <Else />}</If><For entries={items} track="id">{({ item, index }) => ...}</For>; its child is an item method, not a component boundary.<Fn ctx={something}>{(ctx) => ...}</Fn>For has no fallback; for empty states, wrap with <If>.<If condition={someSignal}> into extra .value plumbing.memo(fn) caches only the immediate result of the first fn(...) call in its captured owner scope.Page(props) returns a render function, then that render function receives the concrete renderer and produces host nodes. Therefore memo(Page) caches the page setup result, not its concrete rendered node.memo(Page) value is removed from Dynamic, its mount-specific render scope is disposed. Re-selecting it uses the same cached setup result but creates a fresh mounted subtree. A setup-created keyed For must rebuild its row cache for that new mount.useMemo(fn) is the factory form for defining the helper outside a component and obtaining a correctly scoped memo(fn) inside each component instance.keepAlive(Page) caches both the setup result and the first concrete renderer result. Removing it from Dynamic detaches the node while its effects, subscriptions, and keyed For cache remain live; re-selecting it reattaches the exact same node.useKeepAlive(Page) is the factory form for defining a keep-alive helper outside components. Invoke the returned factory once inside each intended owner; every invocation creates an independently owned retained component template rather than sharing a concrete node globally.keepAlive inside the scope that should own the retained subtree. Disposing that owner must dispose the retained subtree and every nested effect or row scope.memo for one-time computation/setup and keepAlive only for intentional keep-alive UI. Neither is a substitute for correct signal tracking.import { Dynamic, keepAlive, signal } from 'refui'
const current = signal(null)
const App = () => {
const PlayerPage = keepAlive(Player)
current.value = PlayerPage
return <Dynamic is={current} />
}
import { Dynamic, signal, useKeepAlive } from 'refui'
const preparePlayerPage = useKeepAlive(Player)
const App = () => {
const current = signal(null)
const PlayerPage = preparePlayerPage()
current.value = PlayerPage
return <Dynamic is={current} />
}
import { signal, $, If, For } from 'refui'
const App = () => {
const items = signal([{ id: 1, name: 'A' }])
return (
<If condition={$(() => items.value.length)}>
{() => <For entries={items} track="id">{({ item }) => <div>{item.name}</div>}</For>}
{() => <div>Empty</div>}
</If>
)
}
<Async future={promise} fallback={...} catch={...}> for a single promise boundary. Its resolved child receives { result }.<Suspense> groups multiple async descendants under one fallback.lazy(() => import(...)) caches component-module resolution; pass a symbol name for named exports and pair it with an async fallback boundary.Transition retains the current result while coordinating pending, leaving, entering, and entered state for the next result.await are not tracked unless code is deliberately restored into a captured/frozen context.Use context for shared subtree values. If consumers must react to changes, provide a signal as the context value.
import { signal, $, createContext, useContext } from 'refui'
const Theme = createContext(signal('light'), 'Theme')
const Button = () => {
const theme = useContext(Theme)
return <button class:dark={$(() => theme.value === 'dark')}>OK</button>
}
Non-Reflow/custom renderer: wrap Provider children in a function so they inherit context: <Theme value={x}>{() => <Button />}</Theme>.
jsx: 'automatic' plus jsxImportSource: 'refui'. Components normally return JSX directly; the automatic runtime creates renderer-agnostic Reflow functions that a concrete renderer resolves later.jsxFactory: 'R.c' and jsxFragment: 'R.f' (or file pragmas). Components normally return (R) => JSX, making concrete renderer selection explicit.createDOMRenderer(defaults) once at the entry point, then call renderer.render(target, App).createHTMLRenderer(), produce a node, and call serialize(node). The node remains reactive while its owner is live, but each serialized string is only a snapshot.getParent(node) and own ordinary-node parentage. Without it, the core tracks parentage. A renderer that owns parents must move already-parented ordinary nodes consistently in appendNode and insertBefore.import { createDOMRenderer } from 'refui/dom'
import { defaults } from 'refui/browser'
createDOMRenderer(defaults).render(document.getElementById('app'), App)
on:click={fn} (+ on-once:*, on-passive:*, on-capture:*)class:active={boolOrSignal}, style:color={valueOrSignal}attr:* (SVG/read-only), prop:* (force property write)m:* for reusable DOM behaviors (renderer-registered handlers)When implementing a requirement, prefer rEFui’s built-in primitives (signals/components/extras/renderers) over custom plumbing. Only fall back to a custom implementation when:
m:*) or small reusable component.jsx: 'automatic' + jsxImportSource: 'refui' (Vite/esbuild) or jsxImportSource: "refui" (tsconfig/Bun).jsxFactory: 'R.c' + jsxFragment: 'R.f' (Vite/esbuild) or /** @jsx R.c */ file pragmas.createDOMRenderer(defaults) from refui/dom + refui/browser (or refui/presets/browser in older repos).createHTMLRenderer() from refui/html, then serialize().refui/reflow (often injected via jsxInject: import { R } from 'refui/reflow' in classic mode).package.json or the lockfile. Match the import paths and APIs already used by the project; if an API described here is absent from the installed package, do not invent it.The optional scripts/refui-audit.mjs command can scan JSX mode and common .value mistakes, but the workflow must not depend on that script.
This skill remains sufficient for ordinary rEFui work. When an API, version-specific behavior, or repository-level detail is still ambiguous after checking the installed package version and exports, use external documentation lookup when those tools are available:
mcp__context7__resolve-library-id and libraryName: "refui".mcp__context7__query-docs rather than requesting a broad overview.mcp__deepwiki__read_wiki_structure, then mcp__deepwiki__ask_question on SudoMaker/rEFui, such as to find where a behavior is implemented or documented.Treat lookup results as supporting evidence, not a substitute for the target project's installed version or a public-API reproduction. If the tools are unavailable, continue from this guide and the installed package; do not require a linked local document.
signal; derived state: $/computed; reactive work: watch; setup with returned cleanup: useEffect; teardown only: onDispose.If; keyed lists: For; positional list reuse: UnKeyed; inline replacement scope: Fn; changing component/tag: Dynamic; explicit component instance: createComponent plus Render.memo, or useMemo for a module-level factory; retained concrete subtree across detach/reattach: keepAlive, or useKeepAlive for a module-level factory; reusable managed slots: createCache/Cached.Async; grouped promises: Suspense; lazy module: lazy; old/new handoff: Transition.createPortal, which returns inlet and outlet components.Parse; positional lists: UnKeyed; web-component boundary: defineCustomElement.onCondition(source) rather than one computed equality per row.extract or derivedExtract.createDefer or createSchedule; use returned cancellation/disposal paths.createPortal() from refui/extras returns [Inlet, Outlet]. Content produced by inlets is rendered at the outlet and remains disposed with its logical owner.Parse is an escape hatch for parsed/structured source. Sanitize untrusted markup before parsing or emitting raw HTML.rawHTML bypasses escaping and must receive trusted content only.defineCustomElement creates a custom-element boundary whose attributes map to reactive props and whose connected/disconnected lifecycle owns the rendered component.m:* macros for DOM-only behaviors such as focus traps, click-outside, and scroll locking; the macro must clean up listeners or host resources.refurbish integration for the project’s bundler; application components should not hand-write import.meta.hot bookkeeping.import { refurbish } from 'refurbish/vite' and plugins: [refurbish()]. Bun uses the refurbish/bun plugin.$ref or expose for stable node/component handles. HMR wrapping may change the immediate object returned by renderer.render() or createComponent().useState, hooks, VDOM assumptions, $: blocks, etc.). Map them to rEFui signals/effects.<div>{count}</div><div>{$(() => Count: ${count.value})}</div> or <div>{computed(() => ...)}</div>.value in JSX: <div>{count.value}</div> (evaluates once, won’t update)<If condition={flag}> when flag is already a signal/computed<If condition={$(() => count.value > 0)}> for a derived condition<If condition={flag}> as a reactivity smell by itselfawait nextTick() when you must observe derived updates.const x = signal(initial)const y = $(() => /* uses x.value */) (or computed(() => ...))watch(fn) for reactive computations; useEffect(setup) for setup+cleanup; onDispose(cleanup) for teardown.<For entries={items} track="id">{({ item }) => ...}</For>UnKeyed from refui/extras/unkeyed.jssig.trigger() after mutation.<Async future={promise} fallback={...} catch={...}>{({ result }) => ...}</Async><Suspense> for grouping async subtreesasync components are supported; pair with fallbacks when needed.on:click={...}, plus options on-once:*, on-passive:*, on-capture:*attr: for SVG or when a DOM prop is read-only; use prop: to force a property set.class:x={boolSignal}, style:color={valueOrSignal}m:name={value} where name is registered on the renderer.$ref={sig} to receive a node/instance in sig.value$ref={(node) => ...} callback formexpose prop for imperative child handles (v0.8.0+).extract/derivedExtract to reduce fan-out.m:* rather than duplicating manual DOM code.<For> unless you have a measured reason to use unkeyed.npm/pnpm/yarn/bun) and language (JS/TS). Do not ask runtime.refui latest from npm unless the user specifies otherwise.esbuild: { jsx: 'automatic', jsxImportSource: 'refui' }. For TS/TSX, use "jsx": "react-jsx" and "jsxImportSource": "refui" when TypeScript performs the transform; do not configure two competing JSX transforms..jsx/.tsx for files containing JSX. Create the host renderer once in the entry file and mount the root component there.createDOMRenderer from refui/dom, imports defaults from refui/browser, creates the renderer, and calls renderer.render(document.getElementById('app'), App).refurbish only when HMR is wanted and configure its bundler plugin as described above.nextTick() before asserting downstream effects or rendered output.memo, keepAlive, useKeepAlive, Dynamic, async boundaries, lists, listeners, and external resources.DocumentFragment semantics. Test host-specific behavior against the actual host contract, and test HTML behavior with the HTML renderer.Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer