React bindings and SchemaRenderer component for Object UI.
- ⚛️ SchemaRenderer - Main component for rendering Object UI schemas
- 🪝 React Hooks - Hooks for accessing renderer context
- 🔄 Context Providers - React Context for state management
- 📦 Tree-Shakable - Import only what you need
npm install @object-ui/react @object-ui/corePeer Dependencies:
react^18.0.0 || ^19.0.0react-dom^18.0.0 || ^19.0.0
import { SchemaRenderer } from '@object-ui/react'
const schema = {
type: 'text',
value: 'Hello, Object UI!'
}
function App() {
return <SchemaRenderer schema={schema} />
}import { SchemaRenderer } from '@object-ui/react'
const schema = {
type: 'form',
body: [
{
type: 'input',
name: 'name',
label: 'Name',
value: '${user.name}'
}
]
}
const data = {
user: { name: 'John Doe' }
}
function App() {
return <SchemaRenderer schema={schema} data={data} />
}import { SchemaRenderer } from '@object-ui/react'
function App() {
const handleSubmit = (data) => {
console.log('Form submitted:', data)
}
return (
<SchemaRenderer
schema={formSchema}
onSubmit={handleSubmit}
/>
)
}Injects the host's data source (and optional capabilities) into every renderer below it:
import { SchemaRendererProvider } from '@object-ui/react'
<SchemaRendererProvider
dataSource={adapter}
// Optional: host-authenticated fetch used by `provider: 'api'` view data
// sources, so custom endpoints carry the same credentials (Authorization,
// tenant, locale headers) as the native data channel. When omitted,
// ApiDataSource falls back to the bare global fetch (cookies only).
apiFetch={authenticatedFetch}
>
<SchemaRenderer schema={schema} />
</SchemaRendererProvider>Nested providers inherit apiFetch from their parent when they don't supply
their own, so re-wrapped subtrees (embedded pages, preview surfaces) keep the
host's authentication.
Access the current schema context:
import { useSchemaContext } from '@object-ui/react'
function MyComponent() {
const { data, updateData } = useSchemaContext()
return <div>{data.value}</div>
}Consume PageComponentSchema.dataSource — the spec's per-element data binding
({ object, view?, filter?, sort?, limit? }) — in a block that has its own key
names. useElementDataSource resolves the binding (fetching the object's saved
views so view can be matched); these two apply the composed result to the
block's schema and render the two non-final states.
import { ElementDataSourceGate } from '@object-ui/react'
// `mapping` names ONLY the keys this block reads. A composed value written onto
// a key the block ignores would be accepted and silently dropped — the defect
// the binding exists to remove.
const OBJECT_GRID_BINDING = {
columns: true, // the view's FIELD list may fill `schema.columns`
filter: true, // AND-combined, never replaced ("additional criteria")
sort: true,
limit: 'pagination.pageSize' as const,
}
const ObjectGridRenderer = ({ schema, ...props }) => (
<ElementDataSourceGate schema={schema} mapping={OBJECT_GRID_BINDING} testId="object-grid">
{(bound) => <ObjectGrid schema={bound} {...props} />}
</ElementDataSourceGate>
)object lands on objectName by default (pass object: 'apiName' for another
key, or object: false for a block that reads the composed binding itself).
Precedence: binding keys beat the component's own, view-sourced values are only a
baseline the component's own key overrides, and filter AND-combines all three.
A view name that does not resolve renders a configuration error rather than
falling back to the object's full scope. Use useElementDataSourceSchema (plus
the exported ElementDataSourceErrorPanel / ElementDataSourceLoadingPanel) when
a block cannot be wrapped — a renderer whose hooks must run before the panels.
The settled-schema RESOLUTION half shared by ObjectKanban / ObjectView /
ObjectCalendar's fetch-gate hand copies (objectui#6482). Tracks whether an
object's definition has finished resolving FOR THE KEY THE CURRENT RENDER IS
ASKING ABOUT — ready and def are two views of one piece of state, so a
stale key can never read as ready. GATE PLACEMENT — which effect actually
waits on ready — stays a per-component decision; this hook only owns the
resolution.
import { useSettledSchema } from '@object-ui/react'
function ObjectSomething({ schema, dataSource }) {
const key = schema.objectName ?? ''
const { ready, def } = useSettledSchema(key, dataSource)
useEffect(() => {
if (!ready) return // gate placement is local to this component
// issue the record query, e.g. buildExpandFields(def?.fields)
}, [ready, def])
}Pass dataSource: undefined for a render that should settle immediately with
no definition (e.g. a provider that issues no metadata read at all) instead of
adding a separate enable flag.
There is no registry hook: the registry is a process-level singleton exported
by @object-ui/core, so read it directly. Subscribe to it only when a lazily
registered plugin must trigger a re-render.
import { ComponentRegistry } from '@object-ui/core'
function MyComponent(props: Record<string, unknown>) {
const Component = ComponentRegistry.get('button')
return Component ? <Component {...props} /> : null
}Access server discovery information including preview mode detection:
import { useDiscovery } from '@object-ui/react'
function MyComponent() {
const { discovery, isLoading, isAuthEnabled, isAiEnabled } = useDiscovery()
// Check if the server is in preview mode
if (discovery?.mode === 'preview') {
console.log('Preview mode active:', discovery.previewMode)
}
// Check if AI service is available
if (isAiEnabled) {
console.log('AI service route:', discovery?.services?.ai?.route)
}
return <div>Server: {discovery?.name}</div>
}| Property | Type | Description |
|---|---|---|
name |
string |
Server name |
version |
string |
Server version |
mode |
string |
Runtime mode (e.g. 'development', 'production', 'preview') |
previewMode |
object |
Preview mode configuration (present when mode is 'preview') |
services |
object |
Service availability status (auth, data, metadata, ai) |
capabilities |
string[] |
API capabilities |
The previewMode object contains:
| Property | Type | Default | Description |
|---|---|---|---|
autoLogin |
boolean |
true |
Skip login/registration pages |
simulatedRole |
'admin' | 'user' | 'viewer' |
'admin' |
Simulated user role |
simulatedUserName |
string |
'Preview User' |
Display name |
readOnly |
boolean |
false |
Read-only mode |
expiresInSeconds |
number |
0 |
Session duration (0 = no expiry) |
bannerMessage |
string |
— | UI banner message |
NotificationProvider implements the spec NotificationSchema. A notification's
severity picks its icon and tone; its displayType picks the surface that
renders it — and each of the five spec types has a distinct one:
displayType |
Presentation | Rendered by | Auto-dismiss |
|---|---|---|---|
toast |
transient overlay | the onToast delegate |
yes |
snackbar |
bottom-anchored bar, one at a time, one action | <NotificationSnackbar /> |
yes |
banner |
page-width strip in the content flow | <NotificationBanners /> |
no |
alert |
blocking acknowledgement dialog (FIFO) | <NotificationAlerts /> |
no |
inline |
in place, at the raising surface | <NotificationInline /> |
no |
The surface components ship in @object-ui/components; mount them where they
belong (a banner is in flow, an inline notification sits next to its raiser).
onToast receives only toast items — it used to receive all five, which is
why every type looked like a toast.
const { notify } = useNotifications()
notify({ title: 'Saved', severity: 'success' }) // toast (spec default)
notify({ title: 'Viewing a draft', severity: 'warning', displayType: 'banner' })
notify({ title: 'Fix 2 fields', severity: 'error', displayType: 'inline', scope: 'contact-form' })A surface component subscribes with useNotificationsByPresentation(type, scope?),
which also registers the surface — raising a banner with no banner surface
mounted warns in dev instead of vanishing.
config is the spec NotificationConfigSchema (defaultPosition,
defaultDuration, maxVisible, stackDirection, pauseOnHover); the legacy
position / stacking spellings still resolve through
resolveNotificationConfig. Three helpers apply it so every surface agrees:
resolveNotificationPosition (a declared position always wins; nothing declared
leaves the surface on its own anchor), visibleNotificationStack (maxVisible +
stackDirection), and the context's pauseAutoDismiss / resumeAutoDismiss
(pauseOnHover). See the
notifications guide.
See full documentation for detailed API reference.
MIT — see LICENSE.