7.4 KiB
Solid Gotchas
Critical
Never use process.exit() directly
This is the most common mistake. Using process.exit() leaves the terminal in a broken state (cursor hidden, raw mode, alternate screen).
// WRONG - Terminal left in broken state
process.exit(0)
// CORRECT - Use renderer.destroy()
import { useRenderer } from "@opentui/solid"
function App() {
const renderer = useRenderer()
const handleExit = () => {
renderer.destroy() // Cleans up and exits properly
}
}
renderer.destroy() restores the terminal (exits alternate screen, restores cursor, etc.) before exiting.
Configuration Issues
Missing bunfig.toml
Symptom: JSX syntax errors, components not rendering
SyntaxError: Unexpected token '<'
Fix: Create bunfig.toml in project root:
preload = ["@opentui/solid/preload"]
Wrong JSX Settings
Symptom: JSX compiles to React, errors about React not found
Fix: Ensure tsconfig.json has:
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "@opentui/solid"
}
}
Build Without Plugin
Symptom: Built bundle has raw JSX
Fix: Add Solid plugin to build:
import solidPlugin from "@opentui/solid/bun-plugin"
await Bun.build({
// ...
plugins: [solidPlugin],
})
Reactivity Issues
Accessing Signals Without Calling
Symptom: Value never updates, shows [Function]
// WRONG - Missing ()
const [count, setCount] = createSignal(0)
<text>Count: {count}</text> // Shows [Function]
// CORRECT
<text>Count: {count()}</text>
Breaking Reactivity with Destructuring
Symptom: Props stop being reactive
// WRONG - Breaks reactivity
function Component(props: { value: number }) {
const { value } = props // Destructured once, never updates!
return <text>{value}</text>
}
// CORRECT - Keep props reactive
function Component(props: { value: number }) {
return <text>{props.value}</text>
}
// OR use splitProps
function Component(props: { value: number; other: string }) {
const [local, rest] = splitProps(props, ["value"])
return <text>{local.value}</text>
}
Effects Not Running
Symptom: createEffect doesn't trigger
// WRONG - Signal not accessed in effect
const [count, setCount] = createSignal(0)
createEffect(() => {
console.log("Count changed") // Never runs after initial!
})
// CORRECT - Access the signal
createEffect(() => {
console.log("Count:", count()) // Runs when count changes
})
Component Naming
Underscore vs Hyphen
Solid uses underscores for multi-word component names:
// WRONG - React-style naming
<tab-select /> // Error!
<ascii-font /> // Error!
<line-number /> // Error!
// CORRECT - Solid naming
<tab_select />
<ascii_font />
<line_number />
Component mapping:
| Concept | React | Solid |
|---|---|---|
| Tab Select | <tab-select> |
<tab_select> |
| ASCII Font | <ascii-font> |
<ascii_font> |
| Line Number | <line-number> |
<line_number> |
Focus Issues
Focus Not Working
Components need explicit focus:
// WRONG
<input placeholder="Type here..." />
// CORRECT
<input placeholder="Type here..." focused />
Select Not Responding
// WRONG
<select options={["a", "b"]} />
// CORRECT
<select
options={[
{ name: "A", description: "Option A", value: "a" },
{ name: "B", description: "Option B", value: "b" },
]}
onSelect={(index, option) => {
// Called when Enter is pressed
console.log("Selected:", option.name)
}}
focused
/>
Select Events Confusion
Remember: onSelect fires on Enter (selection confirmed), onChange fires on navigation:
// WRONG - expecting onChange to fire on Enter
<select
options={options()}
onChange={(i, opt) => submitForm(opt)} // This fires on arrow keys!
/>
// CORRECT
<select
options={options()}
onSelect={(i, opt) => submitForm(opt)} // Enter pressed - submit
onChange={(i, opt) => showPreview(opt)} // Arrow keys - preview
/>
Control Flow Issues
For vs Index
Use For for arrays of objects, Index for primitives:
// For objects - item is reactive
<For each={objects()}>
{(obj) => <text>{obj.name}</text>}
</For>
// For primitives - use Index, item() is reactive
<Index each={strings()}>
{(str, index) => <text>{index}: {str()}</text>}
</Index>
Missing Fallback
Show requires fallback for proper rendering:
// May cause issues
<Show when={data()}>
<Component />
</Show>
// Better - explicit fallback
<Show when={data()} fallback={<text>Loading...</text>}>
<Component />
</Show>
Cleanup Issues
Forgetting onCleanup
Symptom: Memory leaks, multiple intervals running
// WRONG - Interval never cleared
function Timer() {
const [time, setTime] = createSignal(0)
setInterval(() => setTime(t => t + 1), 1000)
return <text>{time()}</text>
}
// CORRECT
function Timer() {
const [time, setTime] = createSignal(0)
const interval = setInterval(() => setTime(t => t + 1), 1000)
onCleanup(() => clearInterval(interval))
return <text>{time()}</text>
}
Effect Cleanup
createEffect(() => {
const subscription = subscribe(data())
// WRONG - No cleanup
// subscription stays active
// CORRECT
onCleanup(() => subscription.unsubscribe())
})
Store Issues
Mutating Store Directly
Symptom: Changes don't trigger updates
const [state, setState] = createStore({ items: [] })
// WRONG - Direct mutation
state.items.push(newItem) // Won't trigger updates!
// CORRECT - Use setState
setState("items", items => [...items, newItem])
Nested Updates
const [state, setState] = createStore({
user: { profile: { name: "John" } }
})
// WRONG
state.user.profile.name = "Jane"
// CORRECT
setState("user", "profile", "name", "Jane")
Debugging
Console Not Visible
OpenTUI captures console output:
import { useRenderer } from "@opentui/solid"
import { onMount } from "solid-js"
function App() {
const renderer = useRenderer()
onMount(() => {
renderer.console.show()
console.log("Now visible!")
})
return <box>{/* ... */}</box>
}
Tracking Reactivity
Use createEffect to debug:
createEffect(() => {
console.log("State:", {
count: count(),
items: items(),
})
})
Runtime Issues
Use Bun
# WRONG
node src/index.tsx
npm run start
# CORRECT
bun run src/index.tsx
bun run start
Async render()
The render function is async when creating a renderer:
// This is fine - Bun supports top-level await
render(() => <App />)
// If you need the renderer
import { createCliRenderer } from "@opentui/core"
import { render } from "@opentui/solid"
const renderer = await createCliRenderer()
render(() => <App />, renderer)
Common Error Messages
"Cannot read properties of undefined"
Usually a missing reactive access:
// Check if signal is being called
<text>{count()}</text> // Note the ()
// Check if props are being accessed correctly
<text>{props.value}</text> // Not destructured
"JSX element has no corresponding closing tag"
Check component naming:
// Wrong
<tab-select></tab-select>
// Correct
<tab_select></tab_select>
"store is not a function"
Stores aren't called like signals:
const [store, setStore] = createStore({ count: 0 })
// WRONG
<text>{store().count}</text>
// CORRECT
<text>{store.count}</text>