# React feedback components
> Let people file bug reports from your React app and follow the answer inside it, with unstyled components and headless hooks.
`@myna-sh/react/feedback` is the reporter's side of [Myna Feedback](/concepts/reports): a form built from the board's own questions, the list of what somebody has filed, and the thread where they read the answer and say whether it worked.
It runs inside **your** product, which is the point. Myna serves no reporter-facing page: your user is already signed in with you, and Myna's own identity model is GitHub OAuth — not a thing to ask of somebody who reported one bug.
```bash
$ npm install @myna-sh/react @myna-sh/sdk
```
React and `@myna-sh/sdk` are peer dependencies; the package has no others.
## The whole thing
```tsx
import {
FeedbackProvider,
ReportForm,
MyReports,
ReportThread,
} from "@myna-sh/react/feedback";
import "@myna-sh/react/feedback.css"; // optional
export function Support({ user, signature }) {
const [open, setOpen] = useState();
return (
({ appVersion: BUILD.version, route: location.pathname })}
>
setOpen(r.number)} />
{open ? : null}
);
}
```
`board` is only needed when the ingest key is not bound to one. `context` is whatever your application knows and chooses to send; Myna never collects it itself.
## Identity
`signature` comes from your own backend:
```ts
import { signIdentity } from "@myna-sh/sdk/feedback/server";
// Your own API route, for the user this request is already authenticated as.
export async function GET(request) {
const user = await authenticate(request);
return Response.json({
signature: signIdentity(process.env.MYNA_IDENTITY_SECRET, user.id),
});
}
```
Without it a report can still be filed — unless the board sets `requireIdentity` — but **nothing can be read back**. An unsigned claim about who somebody is would let any visitor read another person's reports.
Pass `identify` once you know who is using the application, and drop it on sign-out. The provider pushes the change into the existing client rather than rebuilding it, so signing in mid-session works without a remount; clearing it matters, because otherwise the next person on a shared machine reads the last one's reports.
See [identifying your users](/concepts/collecting-feedback#identifying-your-users) for `properties`, the ceilings, and how the secret is minted and rotated.
## Components
### `ReportForm`
Renders the board's intake form — every question, in the order the board asks them, including the ones that fill the report's own title and description. Nothing is hardcoded, which is what stops this component and a hand-written form from asking the same board two different things.
```tsx
console.log("filed", result.number)}
success={(result) => Thanks — that is #{result.number}.
}
labels={{ submit: "Send it" }}
/>
```
The form arrives one round trip late and renders a status line until it does; a form that has to be corrected after it appears is worse than one that appears a moment later. `labels` covers only what the board does not declare — the button, the confirmation, the loading and failure lines. Reword the questions in board settings.
Validation errors bind back per field: the API answers with a `fields` record keyed by path and the form renders each message under the input it belongs to.
### `MyReports`
Everything this identity has filed, newest activity first.
```tsx
Nothing reported yet.
}
children={(r) => <>{r.title} — {r.status}>}
/>
```
Without a signed identity it renders the empty state rather than an error, because "you have not reported anything" is what a signed-out visitor should see, not a sentence about HMAC.
### `ReportThread`
One report and its conversation, with the two buttons the product exists for.
```tsx
```
"It works now" and "Still broken" appear only while the team is waiting on this person — `awaitingYou`, which is the report sitting in `needs_retest`. Showing them all the time would invite somebody to close a report nobody has looked at yet, and the confirmation would then mean nothing.
What the thread shows is deliberately thin: the title, the body, the status, and the **public** conversation with the team attributed as "team" rather than by name. No priority, no labels, no assignee, no internal notes, no attachment manifest, no board guidance.
## Hooks
Every component is built on these, and they are exported for anyone who would rather render their own.
| Hook | Returns |
|---|---|
| `useFeedback()` | The underlying `FeedbackClient` from `@myna-sh/sdk/feedback` |
| `useBoardSchema(board?)` | `{ data, error, isLoading, refresh }` — the board's questions |
| `useSubmitReport()` | `{ submit, isSubmitting, error, result, reset }` |
| `useMyReports()` | `{ data, error, isLoading, refresh }` — their reports |
| `useReport(number)` | The above, plus `reply`, `confirm`, `reopen`, `isActing` |
```tsx
const { data: schema } = useBoardSchema();
const { submit, isSubmitting, error } = useSubmitReport();
```
There is no shared cache here, unlike the content hooks. A page renders dozens of content reads and one feedback surface; a cache would mostly add a way for somebody to see a stale answer to the question they just asked. Each action refetches, so the pane repaints from the server's answer rather than from a guess about what the action did.
## Styling
Every element carries a `data-myna` attribute and nothing else — no class names, no inline styles, no CSS-in-JS.
```css
[data-myna="submit"] { background: var(--brand); }
[data-myna="field"][data-myna-field="build"] { grid-column: span 2; }
[data-myna="event"][data-myna-author="you"] { text-align: right; }
```
That is not minimalism for its own sake. These components sit inside somebody else's product, next to their buttons and their type; a component that arrives with opinions is a component that has to be fought, and every widget that ships a stylesheet ends up in a specificity war with the application embedding it.
The optional stylesheet is a starting point, driven by three custom properties:
```css
@import "@myna-sh/react/feedback.css";
.support {
--myna-accent: #e2653a;
--myna-radius: 10px;
--myna-font: "Inter", sans-serif;
}
```
Everything else comes from `currentColor` and `color-mix`, so the form inherits the surrounding light or dark surface instead of asserting one.
## Errors
`FeedbackError` from `@myna-sh/sdk/feedback` is re-exported here. It carries `status`, a stable `code`, and a `fields` record keyed by path.
| Code | Meaning |
|---|---|
| [`IDENTITY_REQUIRED`](/errors/identity-required) | The board requires a signed identity, or a read-back call carried none |
| `PERMISSION_DENIED` | The `Origin` is not registered on the project, or the key may not file on that board |
| [`PRODUCT_NOT_ENABLED`](/errors/product-not-enabled) | The project does not run Feedback |
| `RATE_LIMITED` | Too many submissions from this address |
| `VALIDATION_FAILED` | A field ceiling or the board's intake form |