Back to Cline

Solid Gotchas

sdk/apps/cli/.agents/skills/opentui/references/solid/gotchas.md

3.83.07.8 KB
Original Source

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).

tsx
// 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:

toml
preload = ["@opentui/solid/preload"]

Wrong JSX Settings

Symptom: JSX compiles to React, errors about React not found

Fix: Ensure tsconfig.json has:

json
{
  "compilerOptions": {
    "jsx": "preserve",
    "jsxImportSource": "@opentui/solid"
  }
}

Build Without Plugin

Symptom: Built bundle has raw JSX

Fix: Add Solid plugin to build:

typescript
import solidPlugin from "@opentui/solid/bun-plugin"

await Bun.build({
  // ...
  plugins: [solidPlugin],
})

Reactivity Issues

Accessing Signals Without Calling

Symptom: Value never updates, shows [Function]

tsx
// 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

tsx
// 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

tsx
// 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
})

HTML Entity Decoding

Solid's reconciler automatically decodes HTML entities in JSX text content. This means &lt;, &gt;, &amp;, etc. render as their literal characters:

tsx
// These render correctly in Solid
<text>Use &lt;box&gt; for containers</text>  // Displays: Use <box> for containers
<text>A &amp; B</text>                        // Displays: A & B

This applies to text nodes, the content prop, and the text prop.

Component Naming

Underscore vs Hyphen

Solid uses underscores for multi-word component names:

tsx
// WRONG - React-style naming
<tab-select />    // Error!
<ascii-font />    // Error!
<line-number />   // Error!

// CORRECT - Solid naming
<tab_select />
<ascii_font />
<line_number />

Component mapping:

ConceptReactSolid
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:

tsx
// WRONG
<input placeholder="Type here..." />

// CORRECT
<input placeholder="Type here..." focused />

Select Not Responding

tsx
// 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:

tsx
// 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:

tsx
// 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:

tsx
// 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

tsx
// 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

tsx
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

tsx
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

tsx
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:

tsx
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:

tsx
createEffect(() => {
  console.log("State:", {
    count: count(),
    items: items(),
  })
})

Runtime Issues

Use Bun

bash
# 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:

tsx
// 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:

tsx
// 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:

tsx
// Wrong
<tab-select></tab-select>

// Correct
<tab_select></tab_select>

"store is not a function"

Stores aren't called like signals:

tsx
const [store, setStore] = createStore({ count: 0 })

// WRONG
<text>{store().count}</text>

// CORRECT
<text>{store.count}</text>