> ## Documentation Index
> Fetch the complete documentation index at: https://docs.noclick.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Components

> Build interactive React/JSX components that render inside NoClick workflows

# Custom Components

Custom components are React applications that render inside the NoClick interface tab. They run in sandboxed iframes with access to the full `@noclick/sdk` API, Tailwind CSS, and any npm package.

## Creating a Component

### From the Canvas

1. Open the **Interface** tab in the workflow editor
2. Drag **Custom Component** from the UX panel onto the grid
3. Click the node on the canvas to open its config
4. Write JSX in the code editor

### From MCP (Claude Code, Cursor)

```xml theme={null}
<add_node type="interface-html-react" name="dashboard" label="Dashboard" />
<update_config id="dashboard" operation="jsx" fullscreen="true" />
<update_config id="dashboard" field="jsx_source">
import React from 'react';
import ReactDOM from 'react-dom/client';

function App() {
  return <div className="p-4 text-white">Hello from NoClick!</div>;
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</update_config>
```

## How It Works

1. You write JSX/TSX in the node's `jsx_source` config field
2. When the node executes, the backend transpiles it via [Sucrase](https://github.com/alangpierce/sucrase) (runs in QuickJS, \~15ms)
3. An import map is generated with React 19 from [esm.sh](https://esm.sh), Tailwind CSS CDN, and any npm packages you import
4. The `@noclick/sdk` is injected as a base64 data URI in the import map
5. Everything is assembled into an HTML document (`srcdoc`) and rendered in an iframe

## Using npm Packages

Just import them. The transpiler auto-detects imports and maps them to esm.sh:

```jsx theme={null}
import { BarChart, Bar, XAxis, YAxis } from 'recharts';
import { format } from 'date-fns';
import { motion } from 'framer-motion';
```

Packages are bundled with `&bundle` to avoid transitive dependency issues.

## Fullscreen Mode

Set `fullscreen="true"` in the node config to render the component as a full-viewport tab instead of a grid block:

```xml theme={null}
<update_config id="my-component" fullscreen="true" />
```

When multiple fullscreen components exist, tabs appear below the main tab bar. Tabs are:

* **Draggable**: reorder by dragging
* **Renamable**: click the active tab label to edit
* **Persistent**: tab order survives page reloads

## Styling

Tailwind CSS is included automatically. Use standard Tailwind classes:

```jsx theme={null}
<div className="flex flex-col items-center gap-4 p-8 bg-zinc-950 text-white min-h-screen">
  <h1 className="text-2xl font-bold">Dashboard</h1>
  <button className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 rounded-lg">
    Click me
  </button>
</div>
```

## Using the SDK

Import any SDK namespace directly:

```jsx theme={null}
import { nodes, execution, state, auth, resources, dataset } from '@noclick/sdk';
```

### Read Node Output

```jsx theme={null}
const output = await nodes.getOutput('data-fetcher-node-id');
```

### Run a Node and Get Results

```jsx theme={null}
// Fire and forget
execution.runNodesInBackground(['node-id']);

// Run with temporary config overrides
execution.runNodesInBackground([
  { id: 'http-node', config: { url: userInput } }
]);

// Stream results as nodes complete
const stream = execution.runNodesAndGetOutput(['fetcher'], ['chart-data', 'summary']);
stream.on('output', (nodeId, data) => {
  if (nodeId === 'chart-data') setChartData(data);
});
await stream.all();
```

### Persistent State

Requires a **State Manager** node in the workflow:

```jsx theme={null}
await state.set('counter', 42);
const val = await state.get('counter');
await state.update('items', list => [...(list || []), newItem]);
await state.del('counter');

// Subscribe to changes
state.onChange('counter', newVal => setCounter(newVal));
```

### OAuth and Credentials

```jsx theme={null}
// Check if Gmail is connected
const hasGmail = await auth.hasCredential('google_gmail_oauth');

// Trigger OAuth popup (scopes auto-resolved from node schemas)
if (!hasGmail) {
  const cred = await auth.requestCredential('google_gmail_oauth');
}

// Create API key credential
const cred = await auth.createCredential('telegram_bot_token', { token: key });
```

### File Upload

```jsx theme={null}
const { resourceId, uploadUrl } = await resources.upload('photo.jpg', 'image/jpeg', file.size);
await fetch(uploadUrl, { method: 'PUT', body: file });
const downloadUrl = await resources.getUrl(resourceId);
```

### Dataset CRUD

```jsx theme={null}
const dsId = await dataset.create('User Submissions');
await dataset.appendRows(dsId, [{ name: 'Alice', score: 95 }]);
const page = await dataset.getRows(dsId, { limit: 100 });
await dataset.deleteRows(dsId, [rowId]);
```

## Caching

Compiled `srcdoc` is cached in IndexedDB (one slot per node). Components render instantly on tab switch and page reload without re-transpilation. The cache auto-invalidates when `jsx_source` changes. Copied/duplicated nodes share cache via source hash matching.

## Troubleshooting

| Issue                       | Solution                                                                                   |
| --------------------------- | ------------------------------------------------------------------------------------------ |
| Gray iframe on first load   | esm.sh cold start (\~3-5s). Packages are cached after first load.                          |
| `&lt;` in stored JSX        | Use `field="jsx_source"` body syntax in MCP XML. Entity escaping is handled automatically. |
| `{{ }}` in JSX stripped     | Fixed: unresolvable `{{ }}` patterns (like JSX inline styles) are preserved as-is.         |
| Drag/resize stuck on canvas | Fixed: `pointer-events: none` on iframes during drag/resize via CSS.                       |
| SDK method hangs            | All requests timeout after 30s with a descriptive error.                                   |
