Use React for a region with its own interactive state, such as a filterable catalog or editor. Use a Markdown component when reusable markup and slots are enough. PathMX keeps the surrounding content in Sources.
For a custom React integration:
bun add @pathmx/react@beta react@^19 react-dom@^19
bun add --dev @types/react @types/react-dom
Use the package root for PathMX context and Action hooks:
import {
PathMXProvider,
useAction,
useActions,
useRelations,
useSource,
} from "@pathmx/react"
Reusable visual components live in the separate @pathmx/ui package so this
binding package remains presentation-free.
This package requires React 19 or newer.
A component returns { html, clientData } and declares its client with moduleRef.
Core updates the host's attributes while preserving React's children. reactClient
keeps the root alive through updates and unmounts it when the host leaves.
Create plugins/catalog.plugin.ts:
import { defineComponent, moduleRef } from "@pathmx/core"
export default defineComponent({
tag: "x-catalog-app",
async load({ page, view }) {
const sources = await view.query("sources", { from: "/books" })
return {
source: page.id,
books: sources.map(source => ({ source: source.id, href: source.href, title: source.meta.title })),
}
},
render: moduleRef(() => import("./catalog.render.ts")),
client: moduleRef(() => import("./catalog.client.tsx")),
})
Create plugins/catalog.render.ts:
import { html } from "@pathmx/core"
export type CatalogData = {
source: string
books: { source: string; href: string; title: string }[]
}
export default function render({ data }: { data: CatalogData }) {
return {
html: html`<section>
<ul>${data.books.map(book => html`<li><a href="${book.href}">${book.title}</a></li>`)}</ul>
</section>`,
clientData: data,
}
}
Query inside load so reads use the current View and register live dependencies.
The portable renderer receives only the selected JSON data. React receives fresh
data when PathMX updates that host; initial links remain useful without JavaScript.
The client and its initial styles load only when the component renders.
Create plugins/catalog.client.tsx:
import { useState } from "react"
import { reactClient } from "@pathmx/react"
import type { CatalogData } from "./catalog.render"
function CatalogView({ books }: CatalogData) {
const [search, setSearch] = useState("")
const visible = books.filter((book) =>
book.title.toLowerCase().includes(search.trim().toLowerCase()),
)
return <>
<label>Find a book <input type="search" value={search}
onChange={(event) => setSearch(event.currentTarget.value)} /></label>
<ul>{visible.map((book) =>
<li key={book.source}><a href={book.href}>{book.title}</a></li>,
)}</ul>
{!visible.length && <p>No matching books.</p>}
</>
}
export default reactClient((data: CatalogData) =>
<CatalogView key={data.source} {...data} />,
)
The type-only import shares the projection without adding runtime imports.
reactClient supplies the selected client data as React props and owns the shared
browser lifecycle.
The key gives this catalog's filter state to its Source. PathMX can retain a matching app host when navigating between Sources, so new data alone does not reset React state. The same Source keeps its filter through live updates; a different Source gets a fresh filter. For an editor or activity, key by the identity that owns its state, including the Actor or target when relevant. An existing persistence key can serve this purpose. Keep the key stable when only the activity's content changes.
Each mounted root receives a unique prefix for React's useId(), keeping label
and ARIA references distinct when several apps share a document.
Use the component in paths/index.md:
# Books
<x-catalog-app />
Add paths/books/looking-slowly.book.md:
# Looking Slowly
A guide to observation.
Run bun run pmx dev, enter part of the title, then edit the book's heading.
The displayed title updates while the filter stays in place. Navigate to the book
and back to verify mounting and cleanup. The filter is component-local state;
removing its host or changing its key resets it. Import a stylesheet from the
client when the app needs custom presentation; PathMX bundles that CSS with it.
Use the project TypeScript setup
and check types, the rendered page, and the browser console. Lint and export alone
do not execute browser interactions. bun run pmx export --origin https://example.com
produces a static catalog with browser filtering; later Source edits require a new
export. Device-local persistence is a separate application choice.
For saved work, first complete the Input reflection example,
including its Actor/Home setup. The following React component submits to that
same response. Its field is named idea and its response id is reflection.
The component below is an alternative Save control inside an already mounted
React app. The Input example
is the complete runnable form. Pass the active page's admitted SourceDescription
into PathMXProvider; a description copied from another page does not describe
this response's Actions. The catalog above supplies complete app mounting; this
section documents the hook contract for apps that already own a description.
import type { SourceDescription } from "@pathmx/core"
import { PathMXProvider, useAction } from "@pathmx/react"
function SaveResponse() {
const { action, invoke } = useAction("input.submit")
return (
<button
disabled={action?.status !== "available"}
onClick={async () => {
await invoke({
response: "reflection",
values: { idea: "I can explain the next step." },
})
}}
>
Save reflection
</button>
)
}
export function ReflectionApp({
description,
}: {
description: SourceDescription
}) {
return (
<PathMXProvider description={description}>
<SaveResponse />
</PathMXProvider>
)
}
Invocation uses the active ViewConnection installed by the page's runtime. The
same helper works with HTTP and worker hosts; it does not create another transport.
Input accepts nested JSON values. The Action's boundInput supplies defaults;
explicitly supplied fields override that described snapshot in the request. The
host resolves admission again and reapplies its current authoritative bound
context before validation, so a stale client cannot override it.
The returned promise resolves to an ActionReceipt. An unavailable Action rejects
before invocation; host failures retain their ActionFailure outcome. React and
native Action forms use the same browser execution and document reconciliation.
One confirmed execution dispatches one pmx:action completion event. A failed
refresh does not undo the saved Action and uses the runtime's normal delayed-update
recovery; navigation can supersede applying an older refresh without canceling
the mutation.
invokePathMXAction(action, input) provides the same behavior outside a provider.
Ordinary PathMX pages install their HTTP connection automatically. Custom worker
hosts install theirs with
startViewRuntime.
Invoking before a host runtime is installed rejects; static publications do not
implicitly gain a mutation connection.
Declare input: true and a required string name prop on the component definition.
The authored name must refer to a field already declared in the owning Input Block.
Core passes that field to reactClient; inside the editor, useInputField() uses
its assigned scope. Removing the component revokes its write capability.
For an editor spanning several fields, declare their prop keys with
input: ["name", "mark"]. In its custom mount, obtain inputFor(element) from
@pathmx/core/input/browser, pass that capability as PathMXProvider's input
property and select a declared field with useInputField(name). The reactClient
single-field shortcut still uses input: true. A Source description alone does
not grant a field binding; see multi-field editors.
import { useInputField } from "@pathmx/react"
function Quantity() {
const { value, editable, set, issues } = useInputField()
return (
<label>
Quantity
<input
value={String(value ?? "")}
disabled={!editable}
onChange={event => {
const text = event.currentTarget.value
set(text === "" ? undefined : Number.isFinite(Number(text)) ? Number(text) : text)
}}
/>
<span role="status">{issues.join(" ")}</span>
</label>
)
}
The hook subscribes to the shared response owner. set changes a draft; an Input
Submit control saves the response. Unmounting the React view does not discard that
draft. Incomplete numeric text such as "-" remains editable and produces a validation
issue rather than silently restoring an older valid number. undefined clears a field.
The returned snapshot contains value, committed, dirty, editable, issues,
pending, uncertain and conflict, plus the stable set function. Pending saves
can coexist with further edits. A revoked scope becomes non-editable and rejects
writes. The hook requires a declared component binding; it cannot create arbitrary
fields or reach other Sources by name.