docs: clarify raw-app index.tsx must mount App to avoid blank screen

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-07-08 13:18:04 +02:00
co-authored by Claude Opus 4.8
parent 286da005ef
commit aef4db4117
5 changed files with 70 additions and 4 deletions
+15 -1
View File
@@ -5585,7 +5585,21 @@ A raw app has three logical parts:
### Entrypoint
\`index.tsx\` is the bundling entrypoint. It typically renders a top-level \`App\` component. The bundler is esbuild.
\`index.tsx\` is the bundling **and mount** entrypoint (esbuild bundles it; the preview then executes it against an empty \`<div id="root">\`). It MUST itself mount a top-level \`App\` component into \`#root\` — nothing is auto-rendered for you. Keep your UI in \`App.tsx\` (or \`App.svelte\` / \`App.vue\`) and keep \`index.tsx\` as the mount shim.
React:
\`\`\`tsx
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root')!).render(<App />)
\`\`\`
Svelte: \`mount(App, { target: document.getElementById('root')! })\`. Vue: \`createApp(App).mount('#root')\`.
**Never turn \`index.tsx\` into a bare component** (e.g. \`export default function App() { ... }\` with no \`createRoot(...).render(...)\`). A component that is defined but never mounted renders **a blank screen with NO error thrown** — the JSX never executes, so nothing surfaces in the console or the error overlay. If the app is blank, the first thing to check is that \`index.tsx\` actually calls \`createRoot(document.getElementById('root')!).render(<App />)\`.
**Always begin every React file (\`.tsx\`/\`.jsx\`) that uses JSX with \`import React from 'react'\`.** esbuild uses the classic JSX transform, so \`React\` must be in scope wherever JSX appears — a missing import compiles fine but throws \`React is not defined\` at runtime, leaving a blank screen.
@@ -925,6 +925,16 @@ export function prepareAppSystemMessage(customPrompt?: string): ChatCompletionSy
### Frontend
- The frontend is bundled using esbuild with entrypoint \`index.tsx\`
- \`index.tsx\` is also the **mount** entrypoint: it MUST mount a top-level \`App\` into \`#root\` — nothing is auto-rendered. Keep your UI in \`App.tsx\` and keep \`index.tsx\` as the mount shim:
\`\`\`tsx
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root')!).render(<App />)
\`\`\`
(Svelte: \`mount(App, { target: document.getElementById('root')! })\`; Vue: \`createApp(App).mount('#root')\`.)
- **Never replace \`index.tsx\` with a bare component.** A component that is defined but never mounted produces a **blank screen with NO error** (the JSX never runs, so nothing throws). If the app renders blank, first verify \`index.tsx\` calls \`createRoot(document.getElementById('root')!).render(<App />)\`.
- Frontend files are managed separately from backend runnables
- The \`wmill.d.ts\` file is generated automatically from the backend runnables shape
- Begin every React file (\`.tsx\`/\`.jsx\`) that uses JSX with \`import React from 'react'\`. Raw apps bundle with the classic JSX transform, so \`React\` must be in scope wherever JSX is used — a missing import compiles fine but throws \`React is not defined\` at runtime.
+15 -1
View File
@@ -611,7 +611,21 @@ A raw app has three logical parts:
### Entrypoint
\`index.tsx\` is the bundling entrypoint. It typically renders a top-level \`App\` component. The bundler is esbuild.
\`index.tsx\` is the bundling **and mount** entrypoint (esbuild bundles it; the preview then executes it against an empty \`<div id="root">\`). It MUST itself mount a top-level \`App\` component into \`#root\` — nothing is auto-rendered for you. Keep your UI in \`App.tsx\` (or \`App.svelte\` / \`App.vue\`) and keep \`index.tsx\` as the mount shim.
React:
\`\`\`tsx
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root')!).render(<App />)
\`\`\`
Svelte: \`mount(App, { target: document.getElementById('root')! })\`. Vue: \`createApp(App).mount('#root')\`.
**Never turn \`index.tsx\` into a bare component** (e.g. \`export default function App() { ... }\` with no \`createRoot(...).render(...)\`). A component that is defined but never mounted renders **a blank screen with NO error thrown** — the JSX never executes, so nothing surfaces in the console or the error overlay. If the app is blank, the first thing to check is that \`index.tsx\` actually calls \`createRoot(document.getElementById('root')!).render(<App />)\`.
**Always begin every React file (\`.tsx\`/\`.jsx\`) that uses JSX with \`import React from 'react'\`.** esbuild uses the classic JSX transform, so \`React\` must be in scope wherever JSX appears — a missing import compiles fine but throws \`React is not defined\` at runtime, leaving a blank screen.
@@ -249,7 +249,21 @@ A raw app has three logical parts:
### Entrypoint
`index.tsx` is the bundling entrypoint. It typically renders a top-level `App` component. The bundler is esbuild.
`index.tsx` is the bundling **and mount** entrypoint (esbuild bundles it; the preview then executes it against an empty `<div id="root">`). It MUST itself mount a top-level `App` component into `#root` — nothing is auto-rendered for you. Keep your UI in `App.tsx` (or `App.svelte` / `App.vue`) and keep `index.tsx` as the mount shim.
React:
```tsx
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root')!).render(<App />)
```
Svelte: `mount(App, { target: document.getElementById('root')! })`. Vue: `createApp(App).mount('#root')`.
**Never turn `index.tsx` into a bare component** (e.g. `export default function App() { ... }` with no `createRoot(...).render(...)`). A component that is defined but never mounted renders **a blank screen with NO error thrown** — the JSX never executes, so nothing surfaces in the console or the error overlay. If the app is blank, the first thing to check is that `index.tsx` actually calls `createRoot(document.getElementById('root')!).render(<App />)`.
**Always begin every React file (`.tsx`/`.jsx`) that uses JSX with `import React from 'react'`.** esbuild uses the classic JSX transform, so `React` must be in scope wherever JSX appears — a missing import compiles fine but throws `React is not defined` at runtime, leaving a blank screen.
+15 -1
View File
@@ -14,7 +14,21 @@ A raw app has three logical parts:
### Entrypoint
`index.tsx` is the bundling entrypoint. It typically renders a top-level `App` component. The bundler is esbuild.
`index.tsx` is the bundling **and mount** entrypoint (esbuild bundles it; the preview then executes it against an empty `<div id="root">`). It MUST itself mount a top-level `App` component into `#root` — nothing is auto-rendered for you. Keep your UI in `App.tsx` (or `App.svelte` / `App.vue`) and keep `index.tsx` as the mount shim.
React:
```tsx
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root')!).render(<App />)
```
Svelte: `mount(App, { target: document.getElementById('root')! })`. Vue: `createApp(App).mount('#root')`.
**Never turn `index.tsx` into a bare component** (e.g. `export default function App() { ... }` with no `createRoot(...).render(...)`). A component that is defined but never mounted renders **a blank screen with NO error thrown** — the JSX never executes, so nothing surfaces in the console or the error overlay. If the app is blank, the first thing to check is that `index.tsx` actually calls `createRoot(document.getElementById('root')!).render(<App />)`.
**Always begin every React file (`.tsx`/`.jsx`) that uses JSX with `import React from 'react'`.** esbuild uses the classic JSX transform, so `React` must be in scope wherever JSX appears — a missing import compiles fine but throws `React is not defined` at runtime, leaving a blank screen.