Files
skills/docs/ui/react/react-midi/README.md
2026-05-29 08:20:56 +01:00

167 lines
10 KiB
Markdown

# @suaveplan/react-midi - React 19 adapter for the Web MIDI API: <MidiProvider>, hooks (useMidi, useMidiInput, useMidiOutput, useMidiMessages, useMidiDevices, useNRPN, useRPN, useCC14), and small UI primitives (MidiDeviceList, MidiActivityIndicator).
React 19 adapter for the Web MIDI API. Provides a strict-mode-safe
`<MidiProvider>` that requests `MIDIAccess` exactly once, seven
typed hooks for common consumption patterns, and two small UI
primitives (`<MidiDeviceList>`, `<MidiActivityIndicator>`).
<!-- begin:auto-docs -->
## Documentation tree
- [midi-activity-indicator](./src/components/midi-activity-indicator/midi-activity-indicator.md) — `<MidiActivityIndicator>` is a small visual primitive that "blinks" on inbound MIDI traffic. It subscribes to the selec…
- [midi-activity-indicator](./src/components/midi-activity-indicator/midi-activity-indicator.md) — `<MidiActivityIndicator>` is a small visual primitive that "blinks" on inbound MIDI traffic. It subscribes to the selec…
- [midi-device-list](./src/components/midi-device-list/midi-device-list.md) — `<MidiDeviceList>` is a small, accessible list of the available MIDI devices for a chosen kind (inputs or outputs). It…
- [midi-device-list](./src/components/midi-device-list/midi-device-list.md) — `<MidiDeviceList>` is a small, accessible list of the available MIDI devices for a chosen kind (inputs or outputs). It…
- [error-codes](./src/error-codes/error-codes.md) — Defines the stable `REACT_MIDI.*` error-code namespace published by `@suaveplan/react-midi`. Every code is generated vi…
- [error-codes](./src/error-codes/error-codes.md) — Defines the stable `REACT_MIDI.*` error-code namespace published by `@suaveplan/react-midi`. Every code is generated vi…
- [hooks](./src/hooks/use-midi-context.md) — Internal helper that returns the live `MidiContextValue` for the nearest `<MidiProvider>` ancestor. It throws `ReactMid…
- [use-cc14](./src/hooks/use-cc14/use-cc14.md) — `useCC14(selector)` exposes the consolidated 14-bit CC change for the selected input. Controllers that need finer resol…
- [use-cc14](./src/hooks/use-cc14/use-cc14.md) — `useCC14(selector)` exposes the consolidated 14-bit CC change for the selected input. Controllers that need finer resol…
- [use-midi](./src/hooks/use-midi/use-midi.md) — `useMidi()` is the most general hook in `@suaveplan/react-midi`. It returns the live `MidiContextValue` surface as `{ s…
- [use-midi](./src/hooks/use-midi/use-midi.md) — `useMidi()` is the most general hook in `@suaveplan/react-midi`. It returns the live `MidiContextValue` surface as `{ s…
- [use-midi-devices](./src/hooks/use-midi-devices/use-midi-devices.md) — `useMidiDevices()` returns the live `{ inputs, outputs }` device-list view, re-rendering whenever the provider's `state…
- [use-midi-devices](./src/hooks/use-midi-devices/use-midi-devices.md) — `useMidiDevices()` returns the live `{ inputs, outputs }` device-list view, re-rendering whenever the provider's `state…
- [use-midi-input](./src/hooks/use-midi-input/use-midi-input.md) — `useMidiInput(selector)` returns a typed `MidiPortLike` adapter over a single Web MIDI input. The adapter parses raw by…
- [use-midi-input](./src/hooks/use-midi-input/use-midi-input.md) — `useMidiInput(selector)` returns a typed `MidiPortLike` adapter over a single Web MIDI input. The adapter parses raw by…
- [use-midi-messages](./src/hooks/use-midi-messages/use-midi-messages.md) — `useMidiMessages(selector, options?)` exposes a bounded ring buffer of the most recent parsed MIDI messages from the se…
- [use-midi-messages](./src/hooks/use-midi-messages/use-midi-messages.md) — `useMidiMessages(selector, options?)` exposes a bounded ring buffer of the most recent parsed MIDI messages from the se…
- [use-midi-output](./src/hooks/use-midi-output/use-midi-output.md) — `useMidiOutput(selector)` returns a typed handle for a single Web MIDI output. The handle exposes both `send(bytes)` (r…
- [use-midi-output](./src/hooks/use-midi-output/use-midi-output.md) — `useMidiOutput(selector)` returns a typed handle for a single Web MIDI output. The handle exposes both `send(bytes)` (r…
- [use-nrpn](./src/hooks/use-nrpn/use-nrpn.md) — `useNRPN(selector)` wraps the universal NRPN reducer from the `midi-contract` module and exposes the consolidated `NRPN…
- [use-nrpn](./src/hooks/use-nrpn/use-nrpn.md) — `useNRPN(selector)` wraps the universal NRPN reducer from the `midi-contract` module and exposes the consolidated `NRPN…
- [use-rpn](./src/hooks/use-rpn/use-rpn.md) — `useRPN(selector)` wraps the universal RPN reducer and exposes the consolidated `RPNChange` as React state. RPN ("Regis…
- [use-rpn](./src/hooks/use-rpn/use-rpn.md) — `useRPN(selector)` wraps the universal RPN reducer and exposes the consolidated `RPNChange` as React state. RPN ("Regis…
- [use-midi-context](./src/hooks/use-midi-context.md) — Internal helper that returns the live `MidiContextValue` for the nearest `<MidiProvider>` ancestor. It throws `ReactMid…
- [codec](./src/midi-contract/codec.md) — `codec.ts` provides minimal `decodeMessage` / `encodeMessage` helpers for the MIDI 1.0 wire format used internally by `…
- [midi-message](./src/midi-contract/midi-message.md) — Defines the local mirror of the `@suaveplan/midi` `MidiMessage` discriminated union plus its peer types (`Channel`, `Mi…
- [reducers](./src/midi-contract/reducers.md) — Three small reducers that fold the four-CC NRPN, RPN, and 14-bit CC "dances" into single consolidated events. The Web M…
- [port](./src/port/web-midi-port.md) — Bridges the native Web MIDI `WebMIDIInput` / `WebMIDIOutput` shapes into the structural `MidiPortLike` consumed by the…
- [web-midi-port](./src/port/web-midi-port.md) — Bridges the native Web MIDI `WebMIDIInput` / `WebMIDIOutput` shapes into the structural `MidiPortLike` consumed by the…
- [midi-context](./src/provider/midi-context.md) — Defines the two React contexts that wire `<MidiProvider>` to its descendant hooks. `MidiReactContext` carries the resol…
- [midi-provider](./src/provider/midi-provider.md) — `<MidiProvider>` is the root context provider for `@suaveplan/react-midi`. It calls `navigator.requestMIDIAccess({ syse…
- [telemetry](./src/telemetry/telemetry.md) — Owns the two OTel counters published by `@suaveplan/react-midi`: `react_midi_messages_received_total` for parsed inboun…
- [telemetry](./src/telemetry/telemetry.md) — Owns the two OTel counters published by `@suaveplan/react-midi`: `react_midi_messages_received_total` for parsed inboun…
- [test-setup](./src/test-setup.md) — Per-package Vitest bootstrap loaded via `setupFiles` before the first test in a worker runs. Its only job is to registe…
- [test-utils](./src/test-utils.md) — Provides the deterministic in-memory implementations of the Web MIDI shape used throughout the `@suaveplan/react-midi`…
<!-- end:auto-docs -->
## Install
```bash
bun add @suaveplan/react-midi
```
Peer deps: `react@^19`, `react-dom@^19`.
## Permission model
Web MIDI is gated behind `navigator.requestMIDIAccess`. `<MidiProvider>`
calls it once on mount and surfaces the outcome as a `status`:
- `loading` — request in flight.
- `granted` — `MIDIAccess` resolved.
- `denied` — user rejected, or the runtime threw (e.g. `NotAllowedError`).
- `unsupported` — `navigator.requestMIDIAccess` is undefined (Firefox,
sandboxed iframes, etc.).
SysEx access is opt-in via `<MidiProvider sysex>`. The provider is
StrictMode-safe — it tracks an in-flight ref so React's double-mount
never produces two parallel requests.
## Quickstart
```tsx
import {
MidiProvider,
useMidi,
useMidiInput,
MidiDeviceList,
} from "@suaveplan/react-midi";
function Workspace() {
const { status } = useMidi();
if (status === "loading") return <p>Connecting MIDI…</p>;
if (status === "denied") return <p>MIDI permission denied.</p>;
if (status === "unsupported")
return <p>MIDI not available in this browser.</p>;
return (
<>
<MidiDeviceList kind="input" />
<NoteLogger />
</>
);
}
function NoteLogger() {
const port = useMidiInput("first");
useEffect(() => {
if (port === null) return;
return port.onMessage((msg) => {
if (msg.type === "noteOn") console.log("note", msg.note);
});
}, [port]);
return null;
}
export default function App() {
return (
<MidiProvider>
<Workspace />
</MidiProvider>
);
}
```
## Device-picker example
```tsx
function DevicePicker() {
const [id, setId] = useState<string | null>(null);
return (
<>
<MidiDeviceList
kind="input"
selectedId={id}
onSelect={setId}
/>
{id !== null ? <NoteLogger portId={id} /> : null}
</>
);
}
```
## Hooks reference
- `useMidi()` — `{ status, access, inputs, outputs, error }`.
- `useMidiInput(id | "first")` — typed `MidiPortLike` with parsed
`MidiMessage` events.
- `useMidiOutput(id | "first")` — typed output handle with
`sendMessage(msg)` (encoded) and raw `send(bytes)`.
- `useMidiMessages(id, { capacity?, filter? })` — ring buffer of
recent messages for log/debug UIs.
- `useMidiDevices()` — `{ inputs, outputs }`, re-renders on
`statechange`.
- `useNRPN(id)`, `useRPN(id)`, `useCC14(id)` — high-resolution
reducers folded into React state.
## Error codes
`REACT_MIDI.{ACCESS_DENIED, UNSUPPORTED_PLATFORM, PORT_NOT_FOUND,
SYSEX_NOT_PERMITTED, NO_PROVIDER}`. Thrown as `ReactMidiError`
instances; permission failures are surfaced as a `status` value
rather than thrown so consumers can render friendly fallbacks.
## Telemetry
Two OTel counters tagged with `port`:
`react_midi_messages_received_total{port}` and
`react_midi_messages_sent_total{port}`.
## On the `@suaveplan/midi` dependency
This package ships a local mirror of the `@suaveplan/midi`
`MidiMessage` contract in `src/midi-contract/`. Once
`@suaveplan/midi` graduates to `workspace:^`, the local mirror is
replaced by a re-export from that package without any shape change.