Skip to content

Commit c191008

Browse files
committed
docs: BROS-810: Add info about src parameter for ReactCode
1 parent b460f20 commit c191008

File tree

1 file changed

+232
-31
lines changed

1 file changed

+232
-31
lines changed

docs/source/tags/reactcode.md

Lines changed: 232 additions & 31 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

@@ -144,6 +115,213 @@ You can use:
144115
- **External CSS**: Load via CDN in your component
145116

146117

118+
## External app mode (`src`)
119+
120+
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()`.
121+
122+
```xml
123+
<View>
124+
<ReactCode name="map" src="http://localhost:3000/index.html" style='{"height":"600px"}' />
125+
</View>
126+
```
127+
128+
The `src` value supports task data interpolation (e.g. `src="$app_url"` resolves from task data), or it can be a static URL.
129+
130+
### When to use `src` mode
131+
132+
| Criteria | Inline code | External `src` |
133+
|---|---|---|
134+
| Setup complexity | Zero — just XML | App must be hosted by URL |
135+
| Framework | React only (no JSX) | Anything (React, Vue, vanilla JS, etc.) |
136+
| Build tools | None | Optional (Vite, webpack, etc.) |
137+
| Dependencies | Loaded dynamically via CDN | Loaded however you want |
138+
| Best for | Forms, tables, simple UIs | Maps, canvases, complex visualizations |
139+
| Debugging | Harder (eval-based code) | Standard browser devtools |
140+
| Versioning | No built-in versioning; maintain by updating XML config | Easier to manage versions with your standard development tools (e.g., Git) |
141+
142+
### Communication protocol
143+
144+
Your app runs inside an iframe. Label Studio communicates with it via `postMessage()`. The lifecycle is:
145+
146+
1. Label Studio creates the iframe with your URL
147+
2. Your app loads and sends `{ type: "ready" }` to the parent
148+
3. Label Studio responds with `{ type: "init", tag, data, regions, viewState }`
149+
4. Your app renders the UI using the received data and regions
150+
5. Ongoing bidirectional messages keep regions and view state in sync
151+
152+
#### Messages from Label Studio to your app
153+
154+
| Message | Fields | When sent |
155+
|---|---|---|
156+
| `init` | `tag`, `data`, `code`*, `regions`, `viewState` | After your app sends `ready` |
157+
| `update` | `tag`, `data?`, `regions?`, `viewState?` | Task or annotation switch (pooled iframe reuse) |
158+
| `regions` | `tag`, `regions` | Regions changed (add/remove/select/update) |
159+
| `viewState` | `tag`, `viewState` | Dark mode toggle, screen context change |
160+
161+
\* `code` is an empty string in `src` mode — ignore it.
162+
163+
#### Messages from your app to Label Studio
164+
165+
| Message | Fields | Purpose |
166+
|---|---|---|
167+
| `ready` | _(none)_ | Signals the app is loaded, triggers `init` |
168+
| `addRegion` | `tag`, `value`, `extraData?` | Create a new region |
169+
| `updateRegion` | `tag`, `id`, `value` | Update an existing region's value |
170+
| `deleteRegion` | `tag`, `id` | Delete a region |
171+
| `selectRegions` | `tag`, `ids` | Select regions in Label Studio's outliner |
172+
173+
!!! warning Important
174+
All outgoing messages must include `tag` (received during `init`). Label Studio uses it to route messages to the correct ReactCode instance.
175+
176+
#### Data shapes
177+
178+
**`regions` array** (received from Label Studio):
179+
```javascript
180+
[
181+
{
182+
id: "abc123",
183+
value: { /* your custom payload */ },
184+
selected: false, // selected in outliner
185+
hidden: false, // hidden via eye icon
186+
locked: false, // locked via lock icon
187+
origin: "manual", // "manual" | "prediction" | "prediction-changed"
188+
}
189+
]
190+
```
191+
192+
**`viewState` object**:
193+
```javascript
194+
{
195+
currentScreen: "quick_view", // "quick_view" | "label_stream" | "review_stream" | "side_by_side"
196+
darkMode: false,
197+
}
198+
```
199+
200+
**`extraData` in `addRegion`** (optional):
201+
```javascript
202+
{ displayText: "Human-readable label for the outliner" }
203+
```
204+
205+
### Minimal `src` app template
206+
207+
```html
208+
<!DOCTYPE html>
209+
<html lang="en">
210+
<head>
211+
<meta charset="UTF-8">
212+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
213+
<title>ReactCode src app</title>
214+
</head>
215+
<body>
216+
<div id="app">Loading…</div>
217+
218+
<script>
219+
let tagName = null;
220+
let regions = [];
221+
222+
function post(msg) {
223+
window.parent.postMessage({ ...msg, tag: tagName }, "*");
224+
}
225+
226+
window.addEventListener("message", (e) => {
227+
if (tagName && e.data.tag && e.data.tag !== tagName) return;
228+
229+
switch (e.data.type) {
230+
case "init":
231+
tagName = e.data.tag;
232+
initApp(e.data.data, e.data.regions, e.data.viewState);
233+
break;
234+
case "update":
235+
if (e.data.data !== undefined) updateData(e.data.data);
236+
if (e.data.regions) reconcileRegions(e.data.regions);
237+
if (e.data.viewState) updateViewState(e.data.viewState);
238+
break;
239+
case "regions":
240+
reconcileRegions(e.data.regions);
241+
break;
242+
case "viewState":
243+
updateViewState(e.data.viewState);
244+
break;
245+
}
246+
});
247+
248+
function initApp(data, initialRegions, viewState) {
249+
if (!data) return; // data can be null during pool warm-up
250+
// Initialize your app with task data, render existing regions
251+
}
252+
function updateData(data) { /* handle new task data */ }
253+
function reconcileRegions(newRegions) {
254+
regions = newRegions;
255+
// Reconcile your visual elements with the new regions array
256+
}
257+
function updateViewState(viewState) { /* e.g. toggle dark mode */ }
258+
259+
// Signal readiness — triggers init from Label Studio
260+
window.parent.postMessage({ type: "ready" }, "*");
261+
</script>
262+
</body>
263+
</html>
264+
```
265+
266+
### Handling null `data`
267+
268+
During initialization (and especially with iframe pooling), `data` may be `null` briefly before the real task data arrives. Your app should guard against this:
269+
270+
```javascript
271+
function initApp(data, initialRegions, viewState) {
272+
if (!data) return; // wait for real data
273+
// ... set up your app
274+
}
275+
```
276+
277+
### Testing locally
278+
279+
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).
280+
281+
**Step 1: Serve your app locally**
282+
283+
```bash
284+
# From your app directory
285+
npx serve -l 3000
286+
```
287+
288+
Alternatives: `python3 -m http.server 3000`, `npx http-server -p 3000`, or any dev server (Vite, webpack-dev-server).
289+
290+
**Step 2: Expose it via an HTTPS tunnel**
291+
292+
```bash
293+
# In another terminal
294+
ngrok http 3000
295+
```
296+
297+
ngrok outputs a public HTTPS URL like `https://abc123.ngrok-free.app`. Use that in your config:
298+
299+
```xml
300+
<ReactCode name="myapp" src="https://abc123.ngrok-free.app/index.html" style='{"height":"600px"}' />
301+
```
302+
303+
Alternatives to ngrok: `cloudflared tunnel --url http://localhost:3000` (Cloudflare Tunnel, no account needed), `npx localtunnel --port 3000`.
304+
305+
!!! note
306+
For production, deploy your app to any static hosting (Vercel, Netlify, S3, GitHub Pages, etc.) and use the permanent HTTPS URL.
307+
308+
### Region reconciliation
309+
310+
When Label Studio sends a `regions` update, your app must reconcile its visual state. This is the most important pattern for `src` apps:
311+
312+
1. **Remove** visuals for regions no longer in the array (deleted)
313+
2. **Hide** visuals for regions with `hidden: true`
314+
3. **Create** visuals for new region IDs
315+
4. **Update** position/value for existing regions that changed
316+
5. **Focus/highlight** regions with `selected: true`
317+
318+
!!! warning Important
319+
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.
320+
321+
### Handling task and annotation switches
322+
323+
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.
324+
147325
## Regions API
148326

149327
Your React component receives these props from Label Studio:
@@ -806,6 +984,29 @@ An interface that displays an image and allows adding metadata annotations:
806984
-->
807985
```
808986

987+
### External app: Map markers (src mode)
988+
989+
A Leaflet + OpenStreetMap map where clicking adds markers stored as regions. Uses vanilla JS (no React). The app is loaded via `src`:
990+
991+
```xml
992+
<View>
993+
<ReactCode name="map" src="http://localhost:3000/index.html" style='{"height":"600px"}' />
994+
</View>
995+
```
996+
997+
Task data: `{ "lat": 51.505, "lon": -0.09, "zoom": 13 }`
998+
999+
Region value: `{ "lat": number, "lon": number, "text": string }`
1000+
1001+
Features:
1002+
- Click map to add marker via `addRegion`
1003+
- Click marker to edit text via `updateRegion`
1004+
- Selection syncs with Label Studio outliner via `selectRegions`
1005+
- Pans to marker when selected from the outliner
1006+
- Dark mode support
1007+
1008+
See the full implementation in the repository: `web/apps/labelstudio/src/tags/ReactCode/examples/src-maps/index.html`
1009+
8091010
## Troubleshooting
8101011

8111012
**Code not rendering**

0 commit comments

Comments
 (0)