# serve and render

> Mount a React tree on each SSH connection with serve, or take manual control with render

The UI layer has two entry points. `serve` is the normal one and wires up the whole app. `render` is lower level and hands you a single mounted instance you drive yourself. Both take the same options.

## serve

```ts
serve(factory: (conn) => ReactElement, options?: RenderOptions): App
```

`serve` is the entry point most apps use. `factory` is called once per SSH connection and returns the React element for that session. Each connection gets its own React tree and is isolated from the others. `serve` wires keyboard input, resize, and close for you, and closes the connection when the app exits (for example when a component calls [`useApp().exit()`](/sdk/ui/hooks#useapp)). It returns the running app.

```jsx title="app.jsx"

function App({ id }) {
  return (
    <Box borderStyle="round" padding={1}>
      <Text>hello, your connection id is <Text bold>{id}</Text></Text>
    </Box>
  );
}

serve((conn) => <App id={conn.id} />);
```

Pass the connection through `factory` when a component needs its `id` or initial size. For live size that updates on resize, use [`useWindowSize`](/sdk/ui/hooks#usewindowsize) inside the tree instead.

### Fullscreen

```jsx title="app.jsx"
serve(() => <App />, { fullscreen: true });
```

Fullscreen uses the terminal's alternate screen and a cell diff renderer, so your UI owns the whole viewport and the user's scrollback is restored on exit. Without it, the UI renders inline and grows the terminal as needed.

## render

```ts
render(node: ReactElement, conn, options?: RenderOptions): Instance
```

`render` mounts one element on one connection and returns an instance handle. Unlike `serve`, it does not wire input or resize. You forward those yourself, which is what you want when you are embedding a UI inside an app that already manages its own connection lifecycle with the [core SDK](/sdk/lifecycle).

```jsx title="app.jsx"

const instances = new Map();

createApp()
  .onConnect((conn) => instances.set(conn.id, render(<App />, conn)))
  .onKey((conn, data) => instances.get(conn.id)?.handleData(data))
  .onResize((conn) => instances.get(conn.id)?.resize())
  .onClose((conn) => {
    instances.get(conn.id)?.unmount();
    instances.delete(conn.id);
  })
  .listen();
```

### The instance handle

`render` returns an object with these methods.

| Method | Description |
| --- | --- |
| `rerender(node)` | Re-mount the tree with a new root element. |
| `unmount(error?)` | Tear down the tree. Pass an error to reject `waitUntilExit`. |
| `clear()` | Clear the rendered output from the terminal. |
| `waitUntilExit()` | Returns a promise that resolves when the app exits, or rejects with the unmount error. |
| `handleData(data)` | Feed a raw input string into the tree (this is what drives `useInput`). |
| `resize()` | Re-read the connection size and re-render. Call it from `onResize`. |

Most apps should use `serve`, which calls all of this for you.

## RenderOptions

The options object is shared by `serve` and `render`.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `fullscreen` | `boolean` | `false` | Use the alternate screen and a cell diff renderer. `alternateScreen` is an alias. |
| `exitOnCtrlC` | `boolean` | `true` | Exit the app when the user presses Ctrl-C. While on, Ctrl-C is not delivered to your `useInput` handler. |
| `colors` | `'truecolor' \| '256' \| '16' \| 'none'` | `'truecolor'` | Downsample color output for the client terminal. See [Styling](/sdk/ui/styling#downsampling). |
| `mouse` | `boolean \| 'all'` | `false` | Enable mouse events. Only honored in `fullscreen` mode. |

<Callout title="Tip">
Each connection runs its own renderer, but they share one app isolate. Component state is per connection; module-level variables are shared across all of them. See [Sharing state](/guides/sharing-state).
</Callout>

---

Source: https://docs.voxal.sh/sdk/ui/rendering
