Skip to main content

Symphony JSX Resources

The text/symphony-jsx MIME type provides the richest integration with Symphony. Resources of this type contain React and TypeScript code that Symphony transpiles and renders inside a sandboxed iframe with full access to the platform through SymphonyContext.

When to Use

Use text/symphony-jsx when you want the deepest integration with Symphony and are comfortable writing React and TypeScript. This resource type is ideal when:

  • You want direct access to the NATS connection, KV storage, navigation, and theming through SymphonyContext.
  • You prefer a zero-build-step workflow where Symphony handles transpilation and module resolution.
  • You are building interactive pages with MUI components that should match the Symphony look and feel automatically.

Because Symphony provides the React runtime, MUI, and other core libraries, your code benefits from automatic theming, routing, and lifecycle synchronization. However, this also means you cannot control the versions of those libraries. If your application depends on a specific version of React or on third-party libraries that are incompatible with the version provided by Symphony, use text/html+symphony or text/html instead—these let you bundle and control your own dependencies.

How It Works

When Symphony receives a text/symphony-jsx resource, it:

  1. Wraps the code in an HTML template that includes React, Babel (for in-browser TypeScript transpilation), MUI, and the Symphony client library.
  2. Resolves any ESM import statements by mapping them to the /npm endpoint, which proxies packages from a content delivery network.
  3. Loads the result into a sandboxed iframe with allow-scripts allow-same-origin allow-forms permissions.
  4. Synchronizes routing, theming, and lifecycle events between the iframe and the parent Symphony UI.

This means your code is standard React—you write components with hooks, import third-party libraries, and use JSX—but you do not need a build step. Symphony handles transpilation and module resolution. Your code runs using the versions of React, MUI, and other libraries provided by Symphony.

Registration

page = """
import { useContext } from 'react'
import { SymphonyContext } from '@symphony'

export default function MyPage() {
const ctx = useContext(SymphonyContext)
return <h1>Hello from {ctx.symphonyInfo.name}</h1>
}
"""

ext.add_resource("ui://my_ext/home", "text/symphony-jsx", page)
ext.add_route("/my_ext", "ui://my_ext/home")
Resource home = new Resource()
.uri("ui://my_ext/home")
.mimeType(MimeType.TEXT_SYMPHONY_JSX)
.text(getTemplate("home.tsx"));

SymphonyContext

Every text/symphony-jsx component has access to SymphonyContext, which provides:

import { useContext } from 'react'
import { SymphonyContext } from '@symphony'

const ctx = useContext(SymphonyContext)
PropertyTypeDescription
ncNatsConnection | undefinedNATS WebSocket connection
symphonyInfoSymphonyInfoPlatform name, version, and build info
accessTokenstring | undefinedOAuth bearer token
accountIdentifierstring | undefinedCurrent user's account ID
setTitle(title) => voidSet the page title in the Symphony navigation

NATS Messaging

Use symphonyContext.nc for request/reply communication with backend services:

import { useContext, useEffect, useState } from 'react'
import { SymphonyContext } from '@symphony'

const REQUEST_TIMEOUT = 5000

export default function DataView() {
const { nc, setTitle } = useContext(SymphonyContext)
const [items, setItems] = useState([])

useEffect(() => {
setTitle({ "/my_ext": "Data View" })
}, [setTitle])

useEffect(() => {
if (!nc) return
const load = async () => {
const reply = await nc.request(
'cirata.extensions.my_ext.list',
JSON.stringify({ limit: 100 }),
{ timeout: REQUEST_TIMEOUT }
)
const text = new TextDecoder('utf-8').decode(reply.data)
setItems(JSON.parse(text))
}
load()
}, [nc])

return (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
)
}
warning

Always use nc.request() to communicate with backend services. Do not use HTTP fetch() to call extension services directly—extension services are NATS microservices, not HTTP endpoints.

KV Storage

Access JetStream KV buckets for persistent state:

import { useContext, useEffect, useState } from 'react'
import { SymphonyContext } from '@symphony'
import { Kvm } from '@nats-io/kv'

export default function Settings() {
const { nc } = useContext(SymphonyContext)
const [config, setConfig] = useState(null)

useEffect(() => {
if (!nc) return
const load = async () => {
const kvm = new Kvm(nc)
const bucket = await kvm.open('my_ext_config', { allow_direct: true })
const entry = await bucket.get('settings')
if (entry) setConfig(JSON.parse(entry.string()))
}
load()
}, [nc])

const save = async () => {
if (!nc) return
const kvm = new Kvm(nc)
const bucket = await kvm.create('my_ext_config', { allow_direct: true })
await bucket.put('settings', JSON.stringify(config))
}

return (
<div>
<pre>{JSON.stringify(config, null, 2)}</pre>
<button onClick={save}>Save</button>
</div>
)
}

Importing Third-Party Libraries

Any ESM package available on npm can be imported directly. Symphony resolves imports through its /npm endpoint:

import { DataGrid } from '@mui/x-data-grid'
import { Chart } from 'chart.js'
import dayjs from 'dayjs'

MUI components are available by default and share theming with the parent Symphony UI, including automatic light/dark mode synchronization.

Importing from Extension Modules

Extensions can share code between resources by registering text/symphony-module resources and importing from them:

# Register a shared module (not routable—only importable)
ext.add_resource("ui://my_ext/common", "text/symphony-module", common_code)

# Register a page that imports from the module
ext.add_resource("ui://my_ext/home", "text/symphony-jsx", page_code)

In the page resource:

import { formatDate, cardStyle } from '@symphony/extension/my_ext/common'
import { Box, Typography } from '@mui/material'

export default function Home() {
return (
<Box sx={cardStyle}>
<Typography>Updated: {formatDate(Date.now())}</Typography>
</Box>
)
}

The import path follows the pattern @symphony/extension/<prefix>/<module>, which maps to the resource URI ui://<prefix>/<module>. Modules can export functions, constants, style objects, and React components. They can import from npm packages and @symphony APIs just like page resources.

Page Title

Set the page title displayed in the Symphony breadcrumb navigation using setTitle. The parameter is a map from route path to label:

useEffect(() => {
symphonyContext.setTitle({
"/": "Home",
"/my_ext": "My Extension",
"/my_ext/settings": "Settings"
})
}, [symphonyContext.setTitle])

Child Routes

Pages can define nested routes for sub-navigation:

ext.add_route("/my_ext", "ui://my_ext/home", routes={
"/my_ext/detail": {"uri": "ui://my_ext/detail"},
"/my_ext/settings": {"uri": "ui://my_ext/settings"}
})

Each child route maps to a separate resource that renders as a nested component within the parent page's <Outlet />.

Limitations

  • No build step—Code is transpiled in the browser with Babel. This means some advanced TypeScript features (decorators, const enums) may not be supported.
  • No relative navigation—React Router's relative navigation (e.g., navigate(-1)) is not supported. Use absolute paths.
  • Module resolution—ESM packages served through the /npm CDN proxy, @symphony platform APIs, and @symphony/extension/ module resources are available. CommonJS-only packages will not work.

See Also