[@layout]: ../docs.layout.md

# React applications

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.

---

## Install

For a custom React integration:

```sh
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:

```tsx
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.

---

## Mount a Source-backed app

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`:

```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`:

```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.

---

## Add the catalog client

Create `plugins/catalog.client.tsx`:

```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](../authoring/components.reference.md#browser-module-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.

---

## Supply the catalog content

Use the component in `paths/index.md`:

```md
# Books

<x-catalog-app />
```

Add `paths/books/looking-slowly.book.md`:

```md
# Looking Slowly

A guide to observation.
```

---

## Verify filtering and updates

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](../start/project-structure.guide.md#typecheck-project-code)
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.

---

## Invoke a described Action

For saved work, first complete the [Input reflection example](../authoring/input.reference.md#author-a-response),
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](../authoring/input.reference.md#author-a-response)
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.

```tsx
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`](./core.reference.md#browser-hosts-and-document-runtime).
Invoking before a host runtime is installed rejects; static publications do not
implicitly gain a mutation connection.


---

<!-- id: input-fields -->
## Editing an Input field

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](../authoring/components.reference#multi-field-editors).

```tsx
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.
