Solid
The Solid binding — and three traps that will bite you
@uniview/tui-solid is feature-for-feature equivalent to @uniview/tui-react: same
components, same props, same output. What differs is the reactivity model — and three
things that are genuinely easy to get wrong.
Mount once
The React demo keeps mutable state and calls root.render(...) again after every key. The
Solid one mounts a single time; a signal write is enough, and only the panels that actually
changed are repainted.
const [focused, setFocused] = createSignal(2);
root.render(() => <App focused={focused()} />); // once
// later, from a key handler — no rerender call anywhere:
setFocused(3);Resize needs no re-render either: renderer.resize() invalidates layout and schedules its
own repaint, and the component tree is untouched.
Trap 1 — never write literal <text> JSX
solid-js ships an SVG text intrinsic, and an explicit member beats the renderer's
catch-all index signature. So <text> is type-checked against SVG attributes, not ours.
<text color="red">nope</text> {/* ❌ resolves to solid-js's SVG <text> */}
<Text color="red">yes</Text> {/* ✅ the exported component */}<box> and <richtext> have no such collision, but the capitalized components are the
supported surface.
Trap 2 — never destructure props
Destructuring reads the value once and freezes it — the component stops being reactive. This is not a style rule; a mutation test in the suite fails if you break it.
// ❌ focused is captured at creation and never updates
function Panel({ title, focused }: PanelProps) { … }
// ✅
function Panel(props: PanelProps) {
const [local, rest] = splitProps(props, ["title", "focused"]);
return <box {...rest} borderColor={local.focused ? "green" : undefined} />;
}Trap 3 — tsx/esbuild cannot compile this
Solid JSX has to go through babel-preset-solid targeting Uniview's universal renderer.
esbuild — which tsx uses — only knows the React-style transform and would emit
React.createElement-shaped calls. Use the public Vite helper, which configures the transform
and the development/browser conditions for you:
import { univiewSolid } from "@uniview/tui-solid/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [univiewSolid()],
});Keep JSX preserved in TypeScript and load the public JSX augmentation. The compiled output is
then resolved through @uniview/tui-solid/renderer, not an internal package path.
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "solid-js",
"module": "preserve",
"moduleResolution": "bundler",
"strict": true
},
"include": ["src", "vite.config.ts"]
}{ "scripts": { "dev": "vite-node src/main.tsx" } }The compiled JSX imports the public @uniview/tui-solid/renderer entry. It does not require
an internal Uniview renderer package. Add the public JSX augmentation once so TypeScript sees
the terminal intrinsic elements:
import type {} from "@uniview/tui-solid/jsx-runtime";The helper deduplicates solid-js, keeps the renderer in Vite's SSR graph,
and sets the required conditions. Add vite, vite-node, TypeScript, and
@types/node as normal development tooling; they are not terminal-app runtime
dependencies. The supported range is Vite 5.4.21 through 8.1.5: the mandatory
pre-packaging gate runs both Vite 5.4.21 Solid examples, including a
signal-driven second frame, and the packed-consumer gate runs Vite 8.1.5 /
vite-node 6.0.0 on its supported Node versions.
Reactivity notes
useState→createSignal,useMemo→createMemo(no dependency array — Solid tracks reads itself),React.memo→ drop it.- Lists use
<For>/<Index>; there is nokeyprop. Hoverable's render-prop child receives anAccessor<boolean>, so call it:hovered().
A controlled list still needs the "last requested" trick
List is controlled: selectedIndex only becomes the new prop once the parent commits. If
the parent commits asynchronously — a selection owned by a host and echoed back over
RPC, say — two arrow keys in one tick would both read the stale index and collapse into a
single step. List therefore tracks the last requested index internally.
This is easy to dismiss as a React artifact. It is not: Solid compiles selectedIndex={sel()}
to a live getter, so a synchronous parent does update in time — which means the naive test
passes and the bug hides until the parent is async. List handles it for you; if you write
your own controlled widget, do the same.