|
| 1 | +--- |
| 2 | +title: React Pages |
| 3 | +description: Author a page body as real React (kind:'react') or as constrained JSX that is parsed and never executed (kind:'html') — and how to choose between them. |
| 4 | +--- |
| 5 | + |
| 6 | +# React Pages |
| 7 | + |
| 8 | +Most pages in ObjectUI are a **schema tree** — `regions[].components[]` of JSON |
| 9 | +nodes. Two page kinds let you write the body as **source** instead, for layouts |
| 10 | +that are awkward to express as nested JSON: |
| 11 | + |
| 12 | +| `kind` | Source is | Executed? | Author trust | |
| 13 | +|---|---|---|---| |
| 14 | +| `"html"` | Constrained JSX/HTML + Tailwind | **No** — parsed into a schema tree | Untrusted OK | |
| 15 | +| `"react"` | Real React (hooks, handlers, arbitrary JS) | **Yes** — in the main React tree | Trusted only | |
| 16 | + |
| 17 | +Both set `source` and leave `regions` unused. `"jsx"` is a deprecated alias for |
| 18 | +`"html"` and is still accepted. |
| 19 | + |
| 20 | +> Page `kind` also carries the record-page override values `"full"` (default) |
| 21 | +> and `"slotted"` — a different axis, covered in [Slotted Pages](./slotted-pages.md). |
| 22 | +
|
| 23 | +## Choosing between them |
| 24 | + |
| 25 | +Reach for **`kind:'html'`** by default. It is parsed, whitelisted against the |
| 26 | +public block manifest, and never executed, so it is safe for AI-generated and |
| 27 | +customer-authored pages. It covers layout, blocks, and Tailwind styling. |
| 28 | + |
| 29 | +Reach for **`kind:'react'`** only when you need real behaviour the schema tree |
| 30 | +cannot express — local state, computed lists, event handlers wiring one block to |
| 31 | +another, custom data fetching. It runs **without a sandbox**. |
| 32 | + |
| 33 | +## `kind:'react'` |
| 34 | + |
| 35 | +```json |
| 36 | +{ |
| 37 | + "type": "home", |
| 38 | + "name": "project_console", |
| 39 | + "kind": "react", |
| 40 | + "source": "function Page() {\n const [selected, setSelected] = React.useState(null);\n return (\n <div className=\"grid grid-cols-2 gap-4\">\n <ListView objectName=\"showcase_project\" fields={['name', 'status']} onRowClick={(r) => setSelected(r._id)} />\n {selected && <ObjectForm objectName=\"showcase_project\" mode=\"edit\" recordId={selected} />}\n </div>\n );\n}" |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +Written out, that `source` is: |
| 45 | + |
| 46 | +```jsx |
| 47 | +function Page() { |
| 48 | + const [selected, setSelected] = React.useState(null); |
| 49 | + return ( |
| 50 | + <div className="grid grid-cols-2 gap-4"> |
| 51 | + <ListView |
| 52 | + objectName="showcase_project" |
| 53 | + fields={['name', 'status']} |
| 54 | + onRowClick={(r) => setSelected(r._id)} |
| 55 | + /> |
| 56 | + {selected && <ObjectForm objectName="showcase_project" mode="edit" recordId={selected} />} |
| 57 | + </div> |
| 58 | + ); |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +### The security gate |
| 63 | + |
| 64 | +A react page's source is transpiled and evaluated directly in the application — |
| 65 | +no isolation, full access to the page's React tree. The platform assumes page |
| 66 | +authors are reviewed and draft-gated, so the host capability `react-pages` |
| 67 | +defaults **ON**. |
| 68 | + |
| 69 | +A deployment that does not trust its authors turns it off server-side with |
| 70 | +`OS_PAGE_REACT=off` (or `disableCapability('react-pages')` in the host). Pages |
| 71 | +then render an explanatory notice instead of executing. Existing `kind:'html'` |
| 72 | +pages are unaffected. |
| 73 | + |
| 74 | +### What is in scope |
| 75 | + |
| 76 | +Nothing is imported. These identifiers are injected as closure variables: |
| 77 | + |
| 78 | +| In scope | What it is | |
| 79 | +|---|---| |
| 80 | +| `React` | The host's React — call hooks with it (`React.useState`). | |
| 81 | +| The public data blocks | `<ObjectGrid>`, `<ListView>`, `<ObjectForm>`, `<ObjectKanban>`, `<ObjectChart>`, `<ObjectMetric>`, `<Markdown>`, … | |
| 82 | +| `Block` | Escape hatch for anything not injected. | |
| 83 | +| `useAdapter` | The live data source — query/create/update. | |
| 84 | +| `data`, `variables`, `page` | The page's own data, local variables, and schema. | |
| 85 | + |
| 86 | +The block tags come from the curated **public contract** |
| 87 | +(`PUBLIC_BLOCKS`), converted from kebab-case to PascalCase: `object-grid` → |
| 88 | +`<ObjectGrid>`, `record:details` → `<RecordDetails>`. Blocks registered lazily |
| 89 | +are in scope too — you never wait on a plugin chunk to reference one. |
| 90 | + |
| 91 | +**Layout containers are deliberately not injected.** In react mode you compose |
| 92 | +layout with real HTML and Tailwind, which React is better at than a schema-children |
| 93 | +renderer. `<flex>`, `<grid>`, `<card>` and friends have no injected wrapper — use |
| 94 | +`<div className="flex gap-4">`. |
| 95 | + |
| 96 | +### Blocks take flat props |
| 97 | + |
| 98 | +An injected block folds its JSX props into the block's schema, so you write |
| 99 | +flat props rather than a nested `schema` object: |
| 100 | + |
| 101 | +```jsx |
| 102 | +<ObjectGrid objectName="showcase_project" fields={['name', 'status']} pageSize={25} /> |
| 103 | +``` |
| 104 | + |
| 105 | +Function props (`onRowClick`, `onSelect`) are passed through as real callbacks — |
| 106 | +that is how you wire one block to another. |
| 107 | + |
| 108 | +One collision to know about: `type` is both the schema's component |
| 109 | +discriminator and a legitimate prop name on some blocks (a chart's family, for |
| 110 | +instance). The discriminator wins the `type` slot, and your value is preserved |
| 111 | +next to it as `specType` for the block to read. |
| 112 | + |
| 113 | +### `Block` — the escape hatch |
| 114 | + |
| 115 | +Any registered component, including ones outside the public contract: |
| 116 | + |
| 117 | +```jsx |
| 118 | +<Block type="object-tree" objectName="showcase_category" /> |
| 119 | +``` |
| 120 | + |
| 121 | +### Live data |
| 122 | + |
| 123 | +```jsx |
| 124 | +function Page() { |
| 125 | + const adapter = useAdapter(); |
| 126 | + const [rows, setRows] = React.useState([]); |
| 127 | + |
| 128 | + React.useEffect(() => { |
| 129 | + adapter.find('showcase_project', { filters: [['status', '=', 'open']] }).then(setRows); |
| 130 | + }, [adapter]); |
| 131 | + |
| 132 | + return <ul>{rows.map((r) => <li key={r._id}>{r.name}</li>)}</ul>; |
| 133 | +} |
| 134 | +``` |
| 135 | + |
| 136 | +### Source shapes |
| 137 | + |
| 138 | +The page renders the source's **default export**. An implicit `export default` |
| 139 | +is added when the source *starts with* JSX, a `function` declaration, `()`, or |
| 140 | +`class`: |
| 141 | + |
| 142 | +```jsx |
| 143 | +function Page() { return <p/>; } // ✅ |
| 144 | +<p>hi</p> // ✅ |
| 145 | +() => <p>hi</p> // ✅ |
| 146 | + |
| 147 | +const Page = () => <p/>; // ❌ exports nothing |
| 148 | +const Page = () => <p/>; |
| 149 | +export default Page; // ✅ |
| 150 | +``` |
| 151 | + |
| 152 | +The `const Page = …` form does **not** get the implicit export — export it |
| 153 | +explicitly. Getting this wrong reports an error in the page error panel; it does |
| 154 | +not silently render blank. |
| 155 | + |
| 156 | +### When something throws |
| 157 | + |
| 158 | +Transpile errors, evaluation errors, and errors thrown while rendering all |
| 159 | +surface in a **React page error** panel with the message. The error is held |
| 160 | +until the page source or its data changes, so it does not flicker or escape to |
| 161 | +the generic renderer error. |
| 162 | + |
| 163 | +Referencing an identifier that is not in scope is the common case, and reads as |
| 164 | +`ReferenceError: <Name> is not defined` — usually a layout container (not |
| 165 | +injected — use HTML) or a block outside the public contract (use `Block`). |
| 166 | + |
| 167 | +### Page state |
| 168 | + |
| 169 | +A react page keeps its own `useState` across re-renders and across lazy plugin |
| 170 | +loads. Two things reset it, both intentional: a change to `source`, and a change |
| 171 | +to the page's data/variables — the page is genuinely a different page then. |
| 172 | + |
| 173 | +## `kind:'html'` |
| 174 | + |
| 175 | +The constrained tier. Same JSX-looking syntax, but the source is **parsed** |
| 176 | +into a schema tree and rendered through the normal renderer — never executed. |
| 177 | +Only tags in the public block manifest are allowed, props are validated against |
| 178 | +each block's declared inputs, and unknown tags are a hard error at save time. |
| 179 | + |
| 180 | +Use it for anything author- or AI-generated. Expressions are limited to what the |
| 181 | +schema supports (`${data.x}`), and there is no local state or event handling |
| 182 | +beyond the action system. |
| 183 | + |
| 184 | +## Related |
| 185 | + |
| 186 | +- [Slotted Pages](./slotted-pages.md) — `kind:'full'` / `kind:'slotted'` record pages. |
| 187 | +- [Schema Rendering](./schema-rendering.md) — the schema tree the other kinds compile to. |
| 188 | +- [Component Registry](./component-registry.md) — how blocks are registered and what makes one public. |
0 commit comments