Skip to content

Commit f72628e

Browse files
docs: BROS-810: Add info about src parameter for ReactCode (#9427)
Co-authored-by: Caitlin <caitlin@humansignal.com> Co-authored-by: hlomzik <hlomzik@users.noreply.github.com>
1 parent 9c9ec8a commit f72628e

File tree

1 file changed

+210
-33
lines changed

1 file changed

+210
-33
lines changed

docs/source/tags/reactcode.md

Lines changed: 210 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ Importantly, this allows you to continue leveraging Label Studio's annotation ma
2323
| name | string || Unique identifier for the tag (required) |
2424
| [toName] | string || If this is a [self-referencing tag](#Self-referencing-tag), this parameter is required and should match `name` |
2525
| [data] | string || The [task data](#Data-parameter), e.g., `data="$image"` or `data="$text"` |
26-
| [src] | string || URL to an external JavaScript file containing the React component code. Use this as an alternative to inline code. [See more below](#Using-the-src-attribute) |
2726
| [inputs] | string || Defines the JSON schema for the input data (`data`) |
2827
| [outputs] | string || Defines the JSON schema for the [output](#Using-the-outputs-parameter) |
28+
| [src] | string || URL to an external app to load inside the iframe (see [External app mode](#External-app-mode-src)). Supports task data interpolation, e.g. `src="$app_url"` |
2929
| [style] | string || Inline styles or CSS string for the iframe container |
3030
| [classname] | string || Additional CSS classes for the wrapper |
31+
| [allow] | string || iframe permissions policy, e.g. `allow="microphone *; camera *"` |
3132

3233
## ReactCode tag usage notes
3334

@@ -67,37 +68,7 @@ function MyComponent({ React, addRegion, regions, data }) {
6768
}
6869
```
6970

70-
### Using the `src` attribute
7171

72-
By default, you write your React component code inline, directly inside the `<ReactCode>` tag (typically wrapped in a [CDATA section](#CDATA-wrapper)). The `src` attribute provides an alternative approach: instead of embedding the code in the labeling configuration, you can host it at an external URL and reference it.
73-
74-
This works similarly to how a `<script src="...">` tag loads JavaScript from an external file in HTML.
75-
76-
```xml
77-
<!-- Instead of inline code... -->
78-
<ReactCode name="custom" toName="custom" data="$myData">
79-
<![CDATA[
80-
function MyComponent({ React, addRegion, regions, data }) {
81-
// ... your code here
82-
}
83-
]]>
84-
</ReactCode>
85-
86-
<!-- ...you can reference an external file -->
87-
<ReactCode name="custom" toName="custom" data="$myData" src="https://example.com/my-component.js" />
88-
```
89-
90-
The external JavaScript file should export a function component with the same signature as inline code (receiving `React`, `addRegion`, `regions`, `data`, and `viewState` as props).
91-
92-
**When to use `src`:**
93-
94-
- Your component code is large or complex and difficult to manage inside XML
95-
- You want to version and maintain your UI code in a separate repository
96-
- You want to reuse the same component across multiple labeling projects
97-
- You prefer developing in a standard IDE workflow (edit, deploy, reference)
98-
99-
!!! note
100-
When the `src` attribute is provided, any inline code inside the `<ReactCode>` tag is ignored. The component is loaded entirely from the external URL.
10172

10273
## React usage notes
10374

@@ -143,7 +114,6 @@ You can use:
143114
- **Inline styles**: Via the `style` prop in `React.createElement()`
144115
- **External CSS**: Load via CDN in your component
145116

146-
147117
## Regions API
148118

149119
Your React component receives these props from Label Studio:
@@ -253,6 +223,213 @@ Regions created with `ReactCode` follow Label Studio's standard format:
253223

254224
The `value.reactcode` property contains whatever data you passed to `addRegion()`.
255225

226+
## External app mode (`src`)
227+
228+
Instead of writing inline React code, you can load a full standalone web application via the `src` parameter. The app can use any framework, build system, or libraries — it is not limited to React. It communicates with Label Studio via `window.postMessage()`.
229+
230+
```xml
231+
<View>
232+
<ReactCode name="map" src="http://localhost:3000/index.html" style='{"height":"600px"}' />
233+
</View>
234+
```
235+
236+
The `src` value can be a static URL or it can be a variable read from task data (e.g. `src="$app_url"`).
237+
238+
### When to use `src` mode
239+
240+
| Criteria | Inline code | External `src` |
241+
|---|---|---|
242+
| Setup complexity | Zero — just XML | App must be hosted by URL |
243+
| Framework | React only (no JSX) | Anything (React, Vue, vanilla JS, etc.) |
244+
| Build tools | None | Optional (Vite, webpack, etc.) |
245+
| Dependencies | Loaded dynamically via CDN | Loaded however you want |
246+
| Best for | Forms, tables, simple UIs | Maps, canvases, complex visualizations |
247+
| Debugging | Harder (eval-based code) | Standard browser devtools |
248+
| Versioning | No built-in versioning; maintain by updating XML config | Easier to manage versions with your standard development tools (e.g., Git) |
249+
250+
### Communication protocol
251+
252+
Your app runs inside an iframe. Label Studio communicates with it via `postMessage()`. The lifecycle is:
253+
254+
1. Label Studio creates the iframe with your URL
255+
2. Your app loads and sends `{ type: "ready" }` to the parent
256+
3. Label Studio responds with `{ type: "init", tag, data, regions, viewState }`
257+
4. Your app renders the UI using the received data and regions
258+
5. Ongoing bidirectional messages keep regions and view state in sync
259+
260+
#### Messages from Label Studio to your app
261+
262+
| Message | Fields | When sent |
263+
|---|---|---|
264+
| `init` | `tag`, `data`, `code`*, `regions`, `viewState` | After your app sends `ready` |
265+
| `update` | `tag`, `data?`, `regions?`, `viewState?` | Task or annotation switch (pooled iframe reuse) |
266+
| `regions` | `tag`, `regions` | Regions changed (add/remove/select/update) |
267+
| `viewState` | `tag`, `viewState` | Dark mode toggle, screen context change |
268+
269+
\* `code` is an empty string in `src` mode — ignore it.
270+
271+
#### Messages from your app to Label Studio
272+
273+
| Message | Fields | Purpose |
274+
|---|---|---|
275+
| `ready` | _(none)_ | Signals the app is loaded, triggers `init` |
276+
| `addRegion` | `tag`, `value`, `extraData?` | Create a new region |
277+
| `updateRegion` | `tag`, `id`, `value` | Update an existing region's value |
278+
| `deleteRegion` | `tag`, `id` | Delete a region |
279+
| `selectRegions` | `tag`, `ids` | Select regions in Label Studio's labeling interface |
280+
281+
!!! warning Important
282+
All outgoing messages must include `tag` (received during `init`). Label Studio uses it to route messages to the correct ReactCode instance.
283+
284+
#### Data shapes
285+
286+
**`regions` array** (received from Label Studio):
287+
```javascript
288+
[
289+
{
290+
id: "abc123",
291+
value: { /* your custom payload */ },
292+
selected: false, // selected in the labeling interface
293+
hidden: false, // hidden via eye icon
294+
locked: false, // locked via lock icon
295+
origin: "manual", // "manual" | "prediction" | "prediction-changed"
296+
}
297+
]
298+
```
299+
300+
**`viewState` object**:
301+
```javascript
302+
{
303+
currentScreen: "quick_view", // "quick_view" | "label_stream" | "review_stream" | "side_by_side"
304+
darkMode: false,
305+
}
306+
```
307+
308+
**`extraData` in `addRegion`** (optional):
309+
```javascript
310+
{ displayText: "Human-readable label for the labeling interface" }
311+
```
312+
313+
### Minimal `src` app template
314+
315+
```html
316+
<!DOCTYPE html>
317+
<html lang="en">
318+
<head>
319+
<meta charset="UTF-8">
320+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
321+
<title>ReactCode src app</title>
322+
</head>
323+
<body>
324+
<div id="app">Loading…</div>
325+
326+
<script>
327+
let tagName = null;
328+
let regions = [];
329+
330+
function post(msg) {
331+
window.parent.postMessage({ ...msg, tag: tagName }, "*");
332+
}
333+
334+
window.addEventListener("message", (e) => {
335+
if (tagName && e.data.tag && e.data.tag !== tagName) return;
336+
337+
switch (e.data.type) {
338+
case "init":
339+
tagName = e.data.tag;
340+
initApp(e.data.data, e.data.regions, e.data.viewState);
341+
break;
342+
case "update":
343+
if (e.data.data !== undefined) updateData(e.data.data);
344+
if (e.data.regions) reconcileRegions(e.data.regions);
345+
if (e.data.viewState) updateViewState(e.data.viewState);
346+
break;
347+
case "regions":
348+
reconcileRegions(e.data.regions);
349+
break;
350+
case "viewState":
351+
updateViewState(e.data.viewState);
352+
break;
353+
}
354+
});
355+
356+
function initApp(data, initialRegions, viewState) {
357+
if (!data) return; // data can be null during pool warm-up
358+
// Initialize your app with task data, render existing regions
359+
}
360+
function updateData(data) { /* handle new task data */ }
361+
function reconcileRegions(newRegions) {
362+
regions = newRegions;
363+
// Reconcile your visual elements with the new regions array
364+
}
365+
function updateViewState(viewState) { /* e.g. toggle dark mode */ }
366+
367+
// Signal readiness — triggers init from Label Studio
368+
window.parent.postMessage({ type: "ready" }, "*");
369+
</script>
370+
</body>
371+
</html>
372+
```
373+
374+
### Handling null `data`
375+
376+
During initialization (and especially with iframe pooling), `data` may be `null` briefly before the real task data arrives. Your app should guard against this:
377+
378+
```javascript
379+
function initApp(data, initialRegions, viewState) {
380+
if (!data) return; // wait for real data
381+
// ... set up your app
382+
}
383+
```
384+
385+
### Testing locally
386+
387+
During development, you need to make your local app accessible to Label Studio (which runs on HTTPS). A local `http://localhost` URL won't work because browsers block HTTP iframes inside HTTPS pages (mixed content).
388+
389+
**Step 1: Serve your app locally**
390+
391+
```bash
392+
# From your app directory
393+
npx serve -l 3000
394+
```
395+
396+
Alternatives: `python3 -m http.server 3000`, `npx http-server -p 3000`, or any dev server (Vite, webpack-dev-server).
397+
398+
**Step 2: Expose it via an HTTPS tunnel**
399+
400+
```bash
401+
# In another terminal
402+
ngrok http 3000
403+
```
404+
405+
ngrok outputs a public HTTPS URL like `https://abc123.ngrok-free.app`. Use that in your config:
406+
407+
```xml
408+
<ReactCode name="myapp" src="https://abc123.ngrok-free.app/index.html" style='{"height":"600px"}' />
409+
```
410+
411+
Alternatives to ngrok: `cloudflared tunnel --url http://localhost:3000` (Cloudflare Tunnel, no account needed), `npx localtunnel --port 3000`.
412+
413+
!!! note
414+
For production, deploy your app to any static hosting (Vercel, Netlify, S3, GitHub Pages, etc.) and use the permanent HTTPS URL.
415+
416+
### Region reconciliation
417+
418+
When Label Studio sends a `regions` update, your app must reconcile its visual state. This is the most important pattern for `src` apps:
419+
420+
1. **Remove** visuals for regions no longer in the array (deleted)
421+
2. **Hide** visuals for regions with `hidden: true`
422+
3. **Create** visuals for new region IDs
423+
4. **Update** position/value for existing regions that changed
424+
5. **Focus/highlight** regions with `selected: true`
425+
426+
!!! warning Important
427+
Never modify local state optimistically when adding regions. Always post `addRegion` to the parent and wait for the `regions` update. Label Studio assigns the region ID and is the source of truth.
428+
429+
### Handling task and annotation switches
430+
431+
When a user switches tasks or annotations, Label Studio may send an `update` message instead of destroying and recreating the iframe. Your app must handle this by reinitializing its state with the new data and regions. Always handle both `init` and `update` message types.
432+
256433
## Using the `outputs` parameter
257434

258435
You can optionally use the `outputs` parameter to define the expected structure of annotation results. It specifies which fields your interface will produce and what data types they contain.
@@ -839,4 +1016,4 @@ An interface that displays an image and allows adding metadata annotations:
8391016

8401017
- [Spreadsheet Editor](/templates/react_spreadsheet)
8411018
- [Multi-channel audio transcription](/templates/react_audio)
842-
- [Agentic tracing for claims](/templates/react_claims)
1019+
- [Agentic tracing for claims](/templates/react_claims)

0 commit comments

Comments
 (0)