416 lines
7.4 KiB
Markdown
416 lines
7.4 KiB
Markdown
# 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
|
|
})
|
|
```
|
|
|
|
## 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:**
|
|
| 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:
|
|
|
|
```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>
|
|
```
|