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>